20.11.2016 Views

ANDROID APP

Create successful ePaper yourself

Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.

i<br />

รวมโค้ด<br />

Android App<br />

The Android<br />

Developer’s Cookbook<br />

Building Applications with the Android SDK<br />

James Steele<br />

Nelson To<br />

Upper Saddle River, NJ • Boston • Indianapolis • San Francisco<br />

New York • Toronto • Montreal • London • Munich • Paris • Madrid<br />

Cape Town • Sydney • Tokyo • Singapore • Mexico City


MINIMAL SIZE 11 mm.<br />

100<br />

100<br />

-<br />

255<br />

-<br />

111<br />

113<br />

Technology & Business<br />

รวมโค้ด Android App | THE <strong>ANDROID</strong> DEVELOPER’S COOKBOOK<br />

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations<br />

appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all<br />

capitals.<br />

The authors and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume<br />

no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of<br />

the use of the information or programs contained herein.<br />

Authorized translation from the English language edition, entitled <strong>ANDROID</strong> DEVELOPER’S COOKBOOK, THE: BUILDING <strong>APP</strong>LICATIONS WITH<br />

THE <strong>ANDROID</strong> SDK, 1st Edition, 9780321741233 by STEELE, JAMES; TO, NELSON, published by Pearson Education, Inc, publishing as Addison-Wesley<br />

Professional, Copyright © 2011<br />

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including<br />

photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. THAI language edition<br />

published by TRUE DIGITAL CONTENT AND MEDIA CO., LTD., Copyright © 2011<br />

บริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำกัด เป็นผู้ได้รับลิขสิทธิ์ในการจัดแปลเป็นภาษาไทย จากหนังสือ <strong>ANDROID</strong> DEVELOPER’S COOKBOOK, THE:<br />

BUILDING <strong>APP</strong>LICATIONS WITH THE <strong>ANDROID</strong> SDK เขียนโดย JAMES STEELE และ NELSON TO จัดพิมพ์โดย Pearson Education, Inc. ภายใต้<br />

ชื่อ Addison-Wesley สงวนลิขสิทธิ์ฉบับภาษาไทยโดยบริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำกัด พ.ศ. 2554<br />

ห้ามผู้ใดคัดลอก ทำซ้ำ หรือเผยแพร่ส่วนหนึ่งส่วนใดหรือทั้งหมดของหนังสือเล่มนี้ทั้งในฉบับภาษาอังกฤษและฉบับภาษาไทยในทุกสื่อ ไม่ว่าจะเป็นสื่ออิเล็กทรอนิกส์<br />

หนังสือ การบันทึกเสียง หรือที่เก็บรักษาข้อมูลที่บุคคลอื่นสามารถเข้าดูได้มิฉะนั้น จะถูกดำเนินคดีเพื่อลงโทษตามที่กฎหมายบัญญัติไว้สูงสุด<br />

ข้อมูลทางบรรณานุกรมของหอสมุดแห่งชาติ<br />

National Library of Thailand Cataloging in Publication Data<br />

ไพบูลย์ สวัสดิ์ปัญญาโชติ.<br />

The Android Developer’s Cookbook : รวมโค้ด Android App.-- กรุงเทพฯ : ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย, 2554.<br />

340 หน้า.<br />

1. ซอฟต์แวร์คอมพิวเตอร์. 2. โปรแกรมประยุกต์. 3. ระบบปฏิบัติการ (คอมพิวเตอร์)<br />

I. ชื่อเรื่อง.<br />

005.3<br />

ISBN 978-616-90928-5-8<br />

จัดทำและจัดจำหน่ายโดย<br />

สำนักพิมพ์ทรูไลฟ์<br />

เจ้าของ บริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำกัด<br />

TRUE DIGITAL CONTENT AND MEDIA COMPANY LIMITED<br />

121/102, 121/103 อาคารอาร์เอสทาวเวอร์ ชั้น 38<br />

ถนนรัชดาภิเษก แขวงดินแดง<br />

เขตดินแดง กรุงเทพฯ 10400<br />

โทรศัพท์ 0-2642-2733-6 โทรสาร 0-2642-2740<br />

truelifebooks@gmail.com<br />

ติดต่อกองบรรณาธิการ: rapeepan_saw@truecorp.co.th<br />

-<br />

70 ---<br />

109<br />

ที่ปรึกษา มนสินี นาคปนันท์<br />

บรรณาธิการอำนวยการ จิรัฐติกาล สุทธิวรรณารัตน์<br />

บรรณาธิการบริหาร ไพโรจน์ เวชชพิพัฒน์<br />

บรรณาธิการ รพีพรรณ สวัสดิ์ปัญญาโชติ<br />

ผู้เขียน James Steele, Nelson To<br />

ผู้แปล ไพบูลย์ สวัสดิ์ปัญญาโชติ<br />

ฝ่ายผลิต<br />

ทัสฬา นุขุนทด<br />

บรรณาธิการศิลปกรรม ระพีพัฒ พรสี่ภาค<br />

ศิลปกรรมและออกแบบปก Art Team<br />

พิสูจน์อักษร<br />

Proof Reader Team<br />

พิมพ์ที่ บริษัท ทีเอส อินเตอร์พริ้นท์ จำกัด (โทรศัพท์ 0-2174-6055-60)<br />

พิมพ์ครั้งที่ 1 สิงหาคม 2554<br />

พิมพ์ครั้งที่ 2 พฤศจิกายน 2554<br />

พิมพ์ครั้งที่ 3 สิงหาคม 2555<br />

ราคา<br />

295 บาท<br />

สนใจเป็นตัวแทนจำหน่าย หรือสถานศึกษาใดที่ต้องการสั่งซื้อหนังสือเป็นจำนวนมาก พร้อมรับส่วนลดพิเศษสุด<br />

โปรดติดต่อสอบถามได้ที่ คุณยุพา ทองนพ (บริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำกัด) โทร. 0-2642-2733-6 ต่อ 511-517


iii<br />

❖<br />

แด่ Wei ด้วยรักจากใจ<br />

James<br />

แด่คุณแม่อันเป็นที่เคารพรัก<br />

Nelson<br />


iv สารบัญ<br />

รายชื่อบท<br />

บทที่ 1 ก้าวแรกกับแอนดรอยด์ 1<br />

บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์ 23<br />

บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน 51<br />

บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface) 79<br />

บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน 117<br />

บทที่ 6 เทคนิคการทำงานร่วมกับมัลติมีเดีย 147<br />

บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ 169<br />

บทที่ 8 เครือข่าย 195<br />

บทที่ 9 การทำงานร่วมกับข้อมูล 221<br />

บทที่ 10 การระบุตำแหน่ง 251<br />

บทที่ 11 เทคนิคขั้นสูงสำหรับพัฒนาแอพบนแอนดรอยด์ 277<br />

บทที่ 12 การตรวจสอบการทำงานของแอพ 303


สารบัญ<br />

สารบัญ<br />

บทที่ 1 ก้าวแรกกับแอนดรอยด์ 1<br />

วิวัฒนาการของแอนดรอยด์ 1<br />

ลักษณะของแอนดรอยด์ 2<br />

อุปกรณ์ที่ทำงานร่วมกับแอนดรอยด์ 2<br />

ฮาร์ดแวร์ของ HTC 6<br />

ฮาร์ดแวร์ของ Motorola 6<br />

ฮาร์ดแวร์ของ Samsung 6<br />

แท็บเล็ต 7<br />

อุปกรณ์อื่นๆ 7<br />

ความแตกต่างของฮาร์ดแวร์ที่ใช้งานร่วมกับแอนดรอยด์ 8<br />

จอภาพ 8<br />

วิธีการรับข้อมูล 9<br />

เซ็นเซอร์ตรวจจับ 9<br />

คุณสมบัติของแอนดรอยด์ 10<br />

มัลติโปรเซส (Multiprocess) และแอพวิดเจ็ต (App Widget) 11<br />

ทัช มัลติทัช และเจสเจอร์ 11<br />

แป้นพิมพ์แบบฮาร์ดแวร์และซอฟต์แวร์ 11<br />

การพัฒนาแอนดรอยด์ 11<br />

การใช้โค้ดหรือเทคนิคต่างๆ ในหนังสือเล่มนี้ 12<br />

ลักษณะของแอพที่ดี 12<br />

ความเข้ากันได้ของแอพและระบบปฏิบัติการรุ่นใหม่ๆ 13<br />

ประสิทธิภาพในการทำงานของแอพ 13<br />

เครื่องมือที่ใช้ในการพัฒนาแอพ 14<br />

การติดตั้งและอัพเกรด 14<br />

ฟีเจอร์ต่างๆ ของแอนดรอยด์และระดับของ API 15<br />

ระบบจำลองการทำงานและการดีบั๊กแอพแอนดรอยด์ 16<br />

การดีบั๊กแอพแอนดรอยด์ด้วยบริดจ์ 18<br />

การลงทะเบียนและเผยแพร่แอพ 18<br />

Android Market 19<br />

ข้อตกลงและลิขสิทธิ์ของผู้ใช้งานโปรแกรม 19<br />

การทำให้แอพเป็นที่แพร่หลายมากขึ้น 19<br />

วิธีทำให้แอพมีความแตกต่างจากแอพอื่นๆ 20<br />

การกำหนดราคาแอพ 20<br />

การจัดการกับความคิดเห็นที่มีต่อแอพและการอัพเดต 21<br />

ทางเลือกอื่นๆ ที่นอกเหนือจาก Android Market 22<br />

v


vi<br />

สารบัญ<br />

บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์ 23<br />

โครงสร้างการทำงานของแอพแอนดรอยด์ 23<br />

กรรมวิธี: การสร้างโปรเจ็กต์และแอคทิวิตี้ 24<br />

โครงสร้างของไดเร็กทอรีและไฟล์ในโปรเจ็กต์ 26<br />

แอนดรอยด์แพ็คเกจและไฟล์กำหนดคุณลักษณะ (Manifest) 28<br />

การเปลี่ยนชื่อไฟล์ภายในโปรเจ็กต์ 30<br />

วงจรการทำงานของแอคทิวิตี้ 30<br />

กรรมวิธี: การใช้งานแอคทิวิตี้ต่างๆ 31<br />

กรรมวิธี: การทำงานแบบซิงเกิลทาสก์ (Single Task) 33<br />

กรรมวิธี: การหมุนจอภาพ 34<br />

กรรมวิธี: การจัดเก็บและเรียกคืนข้อมูลภายในแอคทิวิตี้ 34<br />

มัลติเพิลแอคทิวิตี้ (Multiple Activities) 35<br />

กรรมวิธี: การใช้ปุ่ม (Button) และฟิลด์ข้อความ (TextView) 36<br />

กรรมวิธี: การเรียกใช้งานแอคทิวิตี้จากอีเวนต์ต่างๆ 37<br />

กรรมวิธี: การเรียกใช้งานแอคทิวิตี้เพื่อแสดงผลลัพธ์ 41<br />

โดยการแปลงเสียงเป็นข้อความ<br />

กรรมวิธี: การสร้างรายการตัวเลือก 43<br />

กรรมวิธี: การใช้งานอินเท็นต์แบบ Implicit เพื่อสร้างแอคทิวิตี้ 44<br />

กรรมวิธี: การส่งผ่านข้อมูลระหว่างแอคทิวิตี้ 46<br />

บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน 51<br />

เธรด (Thread) 51<br />

กรรมวิธี: การเรียกใช้งานเธรดลำดับที่ 2 51<br />

กรรมวิธี: การสร้างแอคทิวิตี้แบบเรียกใช้งานได้ (Runnable) 55<br />

กรรมวิธี: การกำหนดลำดับความสำคัญของเธรด 57<br />

กรรมวิธี: การยกเลิกเธรด 57<br />

กรรมวิธี: การเรียกใช้งานเธรดร่วมกันระหว่างแอพ 58<br />

การส่งข้อมูลระหว่างเธรด: แฮนด์เลอร์ (Handler) 58<br />

กรรมวิธี: การใช้เธรดหลักเพื่อกำหนดช่วงเวลาในการทำงานของทาสก์ 59<br />

กรรมวิธี: การใช้งานนาฬิกาจับเวลาถอยหลัง 61<br />

กรรมวิธี: การแสดงผลในระหว่างที่แอพเริ่มทำงาน 62<br />

เซอร์วิส 64<br />

กรรมวิธี: การสร้างเซอร์วิส 65<br />

การสร้างรีซีฟเวอร์เพื่อรับข้อมูลจากอีเวนต์ 69<br />

กรรมวิธี: การสั่งให้เซอร์วิสเริ่มทำงานเมื่อมีการกดปุ่มกล้องถ่ายรูป 70<br />

แอพวิดเจ็ต (App Widget) 72


สารบัญ<br />

กรรมวิธี: การสร้างแอพวิดเจ็ต 72<br />

การแจ้งเตือน (Alert) 74<br />

กรรมวิธี: การแสดงข้อความแจ้งเตือนบนจอภาพ 74<br />

กรรมวิธี: การใช้งานข้อความแจ้งเตือน 75<br />

กรรมวิธี: การแสดงข้อความบนสเตตัสบาร์ 76<br />

บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface) 79<br />

โครงสร้างของรีซอร์สในไดเร็กทอรี และค่าแอททริบิวต์ที่เกี่ยวข้อง 79<br />

กรรมวิธี: การระบุรีซอร์สเพิ่มเติม 81<br />

วิวและกลุ่มของวิว 82<br />

กรรมวิธี: การสร้างเลย์เอาต์ในโปรแกรม Eclipse 83<br />

กรรมวิธี: การควบคุมความสูงและความกว้างของวัตถุบนจอภาพ 86<br />

กรรมวิธี: การกำหนดความสัมพันธ์ระหว่างเลย์เอาต์และเลย์เอาต์ไอดี 89<br />

กรรมวิธี: การสร้างเลยเอาต์โดยการเขียนโปรแกรม 90<br />

กรรมวิธี: การใช้เธรดเพื่ออัพเดตเลย์เอาต์ 92<br />

การจัดการข้อความ 94<br />

กรรมวิธี: การกำหนดและเปลี่ยนแปลงคุณลักษณะของข้อความ 95<br />

กรรมวิธี: การกรอกข้อความ 98<br />

กรรมวิธี: การสร้างฟอร์ม 100<br />

วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar 101<br />

กรรมวิธี: การใช้งานปุ่มแบบรูปภาพ 102<br />

กรรมวิธี: การใช้งานเช็คบ็อกซ์และปุ่มแบบ Toggle 105<br />

กรรมวิธี: การใช้งานปุ่มตัวเลือกแบบเรดิโอ 108<br />

กรรมวิธี: การสร้างเมนูแบบดร็อปดาวน์ 110<br />

กรรมวิธี: การใช้งาน Progress Bar 112<br />

กรรมวิธี: การใช้งาน Seek Bar 114<br />

บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน 117<br />

การสร้างและการตรวจจับอีเวนต์ 117<br />

กรรมวิธี: การขัดจังหวะการทำงานโดยการกดปุ่มแบบฮาร์ดแวร์ 117<br />

กรรมวิธี: การสร้างเมนู 121<br />

กรรมวิธี: การสร้างเมนูด้วย XML 126<br />

กรรมวิธี: การใช้งานปุ่มค้นหา 127<br />

กรรมวิธี: การตอบสนองต่อการสัมผัสจอภาพ 128<br />

กรรมวิธี: การตรวจจับอีเวนต์เจสเจอร์ 130<br />

กรรมวิธี: การใช้งานมัลติทัช 133<br />

vii


viii<br />

สารบัญ<br />

ไลบรารีขั้นสูงที่ใช้ในส่วนการติดต่อกับผู้ใช้งาน 136<br />

กรรมวิธี: การใช้เจสเจอร์ 136<br />

กรรมวิธี: การวาดภาพแบบ 3 มิติ 140<br />

บทที่ 6 เทคนิคการทำงานร่วมกับมัลติมีเดีย 147<br />

รูปภาพ 148<br />

กรรมวิธี: การจัดการไฟล์รูปภาพ 148<br />

เสียง 154<br />

กรรมวิธี: การสั่งให้เล่นไฟล์เสียงที่เลือกไว้ 154<br />

กรรมวิธี: การบันทึกไฟล์เสียง 157<br />

กรรมวิธี: การจัดการข้อมูลเสียงแบบ Raw 158<br />

กรรมวิธี: การเพิ่มประสิทธิภาพในการใช้งานข้อมูลเสียง 163<br />

กรรมวิธี: การเพิ่มรายการเสียงและการอัพเดตตำแหน่งที่เก็บข้อมูล 165<br />

วิดีโอ 165<br />

บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ 169<br />

กล้องถ่ายรูป 169<br />

กรรมวิธี: การปรับแต่งการทำงานของกล้องถ่ายรูป 170<br />

เซ็นเซอร์ตรวจจับต่างๆ 175<br />

กรรมวิธี: การตรวจสอบทิศทางของโทรศัพท์ 176<br />

กรรมวิธี: การใช้งานเซ็นเซอร์ตรวจจับแสงสว่างและอุณหภูมิ 179<br />

โทรศัพท์ 180<br />

กรรมวิธี: การจัดการโทรศัพท์ 181<br />

กรรมวิธี: การอ่านค่าสถานะของโทรศัพท์ 183<br />

กรรมวิธี: การโทรออกจากเบอร์ที่กำหนด 185<br />

บลูทูธ 185<br />

กรรมวิธี: การเปิดการทำงานของบลูทูธ 186<br />

กรรมวิธี: การค้นหาอุปกรณ์บลูทูธ 187<br />

กรรมวิธี: การจับคู่ระหว่างอุปกรณ์บลูทูธ 188<br />

กรรมวิธี: การเปิดบลูทูธซ็อกเก็ต 188<br />

กรรมวิธี: การใช้งานระบบสั่น 191<br />

กรรมวิธี: การติดต่อระบบเครือข่ายแบบไร้สาย 191<br />

บทที่ 8 เครือข่าย 195<br />

การใช้งาน SMS 195<br />

กรรมวิธี: การส่ง SMS แบบอัตโนมัติเมื่อมีการรับข้อความแล้ว 197


สารบัญ<br />

การทำงานกับข้อมูลบนเว็บ 204<br />

กรรมวิธี: การปรับแต่งเวบบราวเซอร์ 204<br />

กรรมวิธี: การใช้งาน HTTP GET 204<br />

กรรมวิธี: การใช้งาน HTTP POST 209<br />

เครือข่ายสังคม 210<br />

กรรมวิธี: การเชื่อมต่อกับทวิตเตอร์ 210<br />

บทที่ 9 การทำงานร่วมกับข้อมูล 221<br />

Shared Preferences 221<br />

กรรมวิธี: การสร้างและอ่านข้อมูลจาก Shared Preferences 222<br />

กรรมวิธี: การใช้งาน Preferences Framework 222<br />

กรรมวิธี: การปรับเปลี่ยนส่วนการติดต่อกับผู้ใช้งานโดยอ้างอิง 225<br />

จากข้อมูลที่จัดเก็บไว้<br />

กรรมวิธี: การเรียกใช้หน้าจอแสดงข้อตกลงลิขสิทธิ์ของผู้ใช้งาน 228<br />

ฐานข้อมูล SQLite 232<br />

กรรมวิธี: การสร้างแพ็คเกจฐานข้อมูล 232<br />

กรรมวิธี: การใช้งานแพ็คเกจฐานข้อมูล 236<br />

กรรมวิธี: การสร้างไดอารีส่วนตัว 239<br />

Content Provider 243<br />

กรรมวิธี: การสร้าง Content Provider ขึ้นเอง 244<br />

การจัดเก็บและการเปิดไฟล์ 249<br />

บทที่ 10 การระบุตำแหน่ง 251<br />

ขั้นตอนพื้นฐานของการระบุตำแหน่ง 251<br />

กรรมวิธี: การแสดงตำแหน่งล่าสุด 253<br />

กรรมวิธี: การอัพเดตข้อมูลเมื่อมีการเปลี่ยนแปลงตำแหน่ง 254<br />

กรรมวิธี: Listing All Enabled Providers 256<br />

กรรมวิธี: การแปลงข้อมูลพิกัดที่อยู่เป็นข้อมูลที่อยู่ 258<br />

กรรมวิธี: การแปลงข้อมูลที่อยู่เป็นข้อมูลพิกัด 261<br />

การใช้ Google Maps 263<br />

กรรมวิธี: การนำ Google Maps มาใช้ในแอพที่พัฒนาขึ้นเอง 265<br />

กรรมวิธี: การเพิ่มจุดลงบนแผนที่ 267<br />

กรรมวิธี: การเพิ่มวิวลงบนแผนที่ 271<br />

กรรมวิธี: การกำหนดตำแหน่งปัจจุบันของอุปกรณ์ลงบนแผนที่ 274<br />

กรรมวิธี: การกำหนดข้อความแจ้งเตือนเมื่อออกจากพื้นที่ที่กำหนดไว้ 275<br />

ix


x<br />

สารบัญ<br />

บทที่ 11 เทคนิคขั้นสูงสำหรับพัฒนาแอพบนแอนดรอยด์ 277<br />

การสร้างวิวขึ้นเอง (Android Custom View) 277<br />

กรรมวิธี: การปรับแต่งปุ่มกด 277<br />

การใช้งานคอมโพเน็นต์ของแอนดรอยด์แบบที่พัฒนาด้วยภาษา C, C++ 283<br />

กรรมวิธี: การสร้าง Native Component 284<br />

ระบบความปลอดภัยบนแอนดรอยด์ 287<br />

กรรมวิธี: การประกาศและใช้ Permission 288<br />

การสื่อสารข้อมูลระหว่างโปรเซสภายนอกของแอนดรอยด์ 288<br />

กรรมวิธี: การใช้งาน Remote Procedure Call 289<br />

ตัวจัดการข้อมูลสำรองบนระบบปฏิบัติการแอนดรอยด์ 294<br />

กรรมวิธี: การสร้างข้อมูลสำรองจากข้อมูลที่ใช้งาน 294<br />

กรรมวิธี: การสำรองข้อมูลไปไว้ที่คลาวด์ 296<br />

กรรมวิธี: การสั่งให้สำรองและกู้คืนข้อมูล 296<br />

การแสดงภาพเคลื่อนไหวบนระบบปฏิบัติการแอนดรอยด์ 298<br />

กรรมวิธี: การสร้างภาพเคลื่อนไหว 299<br />

บทที่ 12 การตรวจสอบการทำงานของแอพ 303<br />

เครื่องมือดีบั๊กแอพของ Eclipse 303<br />

กรรมวิธี: การกำหนดค่าที่ใช้ในการทำงาน (Run Configuration) 303<br />

กรรมวิธี: การใช้งาน DDMS 304<br />

กรรมวิธี: การตรวจสอบการทำงานของแอพด้วยการกำหนดจุดหยุดการทำงาน 306<br />

เครื่องมือดีบั๊กแอพของชุดพัฒนาโปรแกรม Android SDK 307<br />

กรรมวิธี: การใช้งาน Android Debug Bridge 307<br />

กรรมวิธี: การใช้งาน LogCat 307<br />

กรรมวิธี: การใช้งาน Hierarchy Viewer 309<br />

กรรมวิธี: การใช้งาน TraceView 311<br />

เครื่องมือที่ใช้ในการตรวจสอบการทำงานของแอพบนแอนดรอยด์ 313<br />

กรรมวิธี: การกำหนดค่าเพื่อดีบั๊กด้วย GDB 315


บทนำ<br />

บทนา<br />

แอนดรอยด์เป็นระบบปฏิบัติการบนโทรศัพท์มือถือที่เติบโตเร็วที่สุด อย่ างในปีที่ผ่านมามีการติดตั้ง<br />

ลงในโทรศัพท์มือถือกว่า 30 รุ่นด้วยกัน และในปัจจุบันมีแอพพลิเคชั่น (Application) ที่รองรับมากกว่า<br />

10,000 แอพ ซึ่งมีการพัฒนาแอพใหม่ๆ เพิ่มเติมอยู่ตลอดเวลา โดยลักษณะโครงสร้างของแอนดรอยด์<br />

นั้นมีความยืดหยุ่น สามารถติดตั้งลงในอุปกรณ์ต่างๆ ที่มีความหลากหลายทางฮาร์ดแวร์ได้ รวมถึงอุปกรณ์<br />

ประเภทโทรศัพท์มือถือด้วย เลยทำให้แอนดรอยด์มีความเหมาะสมที่จะนำไปใช้กับอุปกรณ์เหล่านี้มากที่สุด<br />

เดิมทีระบบปฏิบัติการแอนดรอยด์ทำงานได้ดีบนอุปกรณ์ประเภทเน็ตบุ๊ก (Netbook) แต่ด้วยความ<br />

ยืดหยุ่นของระบบปฏิบัติการนี้ เลยมีการนำไปใช้กับอุปกรณ์ต่างๆ ในธุรกิจและอุตสาหกรรมอื่นๆ ด้วย<br />

แอพของแอนดรอย์พัฒนาได้ง่ายเพราะมีไลบรารีรองรับมากมาย ทำให้สามารถสร้างแอพที่ตรงกับความ<br />

ต้องการของผู้ใช้งานได้มากขึ้น แถมยังเพิ่มโอกาสในการพัฒนาแอพของบรรดานักพัฒนายิ่งขึ้นด้วย<br />

ทำไมจึงต้องมีหนังสือเล่มนี้?<br />

ระบบปฏิบัติการแอนดรอยด์มีจุดเด่นตรงที่พัฒนาแอพได้ง่าย และทาง Google ยังได้นำเสนอ<br />

ไลบรารีจำนวนไม่น้อยที่จะมาช่วยพัฒนาแอพในแง่มุมต่างๆ ด้วย ทำให้เราสามารถพัฒนาแอพที่มี<br />

ประสิทธิภาพและซับซ้อนได้ดี แต่ถึงอย่างนั้นนักพัฒนาหลายๆ คนก็ให้ความเห็นว่าหนังสือคู่มือที่จะช่วย<br />

ชี้แนะแนวทางให้เขียนแอพได้แบบละเอียดๆ และเข้าใจง่ายนั้นมีค่อนข้างน้อยและหายากมาก<br />

ระบบปฏิบัติการนี้มีลักษณะเหมือนกับโปรแกรมแบบโอเพ่นซอร์สทั่วไป คือนักพัฒนาสามารถ<br />

ปรับเปลี่ยนและแก้ไขชุดคำสั่งเพื่อให้รองรับกับความต้องการของนักพัฒนาแต่ละรายได้ เลยทำให้ตาม<br />

เว็บบอร์ดต่างๆ ที่มีการพูดคุยเกี่ยวกับการพัฒนาแอพบนแอนดรอยด์ได้นำตัวอย่างการเขียนแอพ, ปัญหา<br />

และวิธีการแก้ไขแบบต่างๆ มาแบ่งปันกัน แต่เวลาจะค้นหาทีก็ไม่ใช่เรื่องง่ายเลย ดังนั้นหากจะมีหนังสือ<br />

สักเล่มที่รวบรวมเนื้อหาที่จำเป็นและครอบคลุมความต้องการของนักพัฒนาส่วนใหญ่ด้วยก็น่าจะดี<br />

ถ้าว่ากันในเชิงปฏิบัติแล้ว ตัวอย่างการเขียนแอพที่ดีและเข้าใจง่ายนั้นจะช่วยให้ผู้ศึกษาเข้าใจได้<br />

อย่างลึกซึ้งและรวดเร็วกว่าการอ่านจากหนังสือเพียงอย่างเดียว การเริ่มต้นเขียนแอพโดยหาแอพตัวอย่าง<br />

ที่มีการทำงานใกล้เคียงกับสิ่งที่คุณต้องการจะเขียน แล้วนำตัวอย่างนั้นมาแก้ไขเพิ่มเติมเพื่อให้ตรงกับ<br />

ความต้องการจะช่วยให้เกิดความคุ้นเคยกับรูปแบบการเขียนชุดคำสั่งในแอนดรอยด์ได้เป็นอย่ างดี ซึ่งใน<br />

หนังสือเล่มนี้เราก็ได้รวบรวมตัวอย่างโค้ดเอาไว้อย่างหลากหลาย พร้อมทั้งคำอธิบายการทำงานของชุด<br />

คำสั่งที่ใช้ในตัวอย่างด้วย<br />

หนังสือเล่มนี้เหมาะสำหรับใครบ้าง?<br />

หนังสือเล่มนี้เหมาะสำหรับผู้ที่เริ่มต้นศึกษาการพัฒนาแอพบนแอนดรอยด์ หรือถ้าคุณมีพื้นฐานการ<br />

เขียนโปรแกรมจาวา (Java) และเคยใช้โปรแกรม Eclipse มาก่อนก็จะช่วยให้เรียนรู้ได้เร็วยิ่งขึ้น เพราะ<br />

โครงสร้างของภาษาที่ใช้พัฒนาแอพบนแอนดรอยด์จะใกล้เคียงกับภาษาจาวา สำหรับเนื้อหาในหนังสือ<br />

เล่มนี้ เราได้จัดแบ่งหมวดหมู่ตามระดับความซับซ้อนของการเขียนแอพเอาไว้เพื่อที่คุณจะได้ศึกษาแต่ละ<br />

บทไปเรื่อยๆ ได้โดยง่าย<br />

xi


xii<br />

บทนา<br />

รูปแบบการนำเสนอของหนังสือเล่มนี้<br />

ตัวอย่างการเขียนแอพในหนังสือเล่มนี้เป็นแบบง่ายๆ ไม่ซับซ้อนมากนัก สามารถศึกษาเองได้<br />

ในแต่ละตัวอย่างจะมีคำอธิบายขั้นตอนการทำงานโดยละเอียด ส่วนในบทที่ 1 และ 2 นั้นจะพูดถึง<br />

ความเป็นมา และทำความรู้จักกับระบบปฏิบัติการแอนดรอยด์ เพื่อเป็นพื้นฐานในการทำความเข้าใจ<br />

โครงสร้างการทำงานของระบบปฏิบัติการนี้<br />

ตัวอย่างเหล่านี้สามารถนำไปใช้อ้างอิงในการพัฒนาแอพได้หลายรูปแบบ โดยในตอนต้นของ<br />

แต่ละบทเรียนจะมีการอธิบายเทคนิคและจุดประสงค์ของตัวอย่างนั้นๆ เอาไว้ และในบางตัวอย่างจะมี<br />

การอ้างอิงถึงเนื้อหาในตัวอย่างอื่นๆ ก่อนหน้านั้นด้วย<br />

เมื่อได้ศึกษาเนื้อหาในหนังสือเล่มนี้ทั้งหมดแล้ว นักพัฒนาจะสามารถ<br />

m เริ่มต้นพัฒนาแอพบนระบบปฏิบัติการแอนดรอยด์ได้<br />

m พัฒนาแอพให้ทำงานบนระบบปฏิบัติการแอนดรอยด์เวอร์ชั่นต่างๆ ได้<br />

m เรียกใช้งานไลบรารีของแอนดรอยด์เพื่อพัฒนาโปรแกรมต่างๆ ได้<br />

m นำตัวอย่างในบทเรียนมาประยุกต์ใช้เพื่อพัฒนาแอพแอนดรอยด์ได้<br />

m พัฒนาแอพโดยใช้แนวคิดที่แตกต่างกันในจุดมุ่งหมายเดียวกันเพื่อเลือกแนวทางที่มี<br />

ประสิทธิภาพในการทำงานที่สุด<br />

m นำเทคนิคต่างๆ มาประยุกต์ใช้ในการพัฒนาแอพได้<br />

โครงสร้างของเนื้อหาในหนังสือเล่มนี้<br />

บทที่ 1 “ก้าวแรกกับแอนดรอยด์” ในบทนี้จะยังไม่มีตัวอย่างการเขียนแอพ แต่จะแนะนำระบบ<br />

ปฏิบัติการแอนดรอยด์และหลักการทำงานเบื้องต้นเพื่อปูไว้เป็นพื้นฐานในการเรียนรู้บทต่อๆ ไป<br />

บทที่ 2 “การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์” จะพูดถึงส่วนประกอบทั้ง 4 ส่วน<br />

ของระบบปฏิบัติการ แอนดรอยด์ รวมถึงการอธิบายโครงสร้างของโปรเจ็กต์ที่ใช้ในการพัฒนาแอพ<br />

และการทำงานของ Activity<br />

บทที่ 3 “เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน” จะอธิบายการทำงานเบื้องหลังของระบบ<br />

ปฏิบัติการแอนดรอยด์ ซึ่งประกอบด้วย เธรด เซอร์วิส รีซีฟเวอร์ และข้อความการแจ้งเตือนต่างๆ<br />

บทที่ 4 “ส่วนการติดต่อกับผู้ใช้งาน (User Interface)” จะเกี่ยวกับการออกแบบส่วนการติดต่อ<br />

กับผู้ใช้งาน และมุมมองต่างๆ<br />

บทที่ 5 “อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน” จะพูดถึงอีเวนต์ต่างๆ ที่เกิดขึ้นใน<br />

ส่วนการติดต่อกับผู้ใช้งาน<br />

บทที่ 6 “เทคนิคการทำงานร่วมกับมัลติมีเดีย” จะเกี่ยวกับการปรับแต่ง บันทึก และเล่นสื่อ<br />

มัลติมีเดีย เช่น ข้อมูลภาพ และข้อมูลเสียง<br />

บทที่ 7 “การติดต่อกับฮาร์ดแวร์ต่างๆ” จะเกี่ยวกับการเรียกใช้งานไลบรารีที่เกี่ยวข้องกับ<br />

ฮาร์ดแวร์ของแอนดรอยด์เพื่อนำมาใช้ในแอพที่พัฒนาขึ้น<br />

บทที่ 8 “เครือข่าย” จะพูดถึงการติดต่อกับแหล่งข้อมูลภายนอกต่างๆ อย่างเช่น เว็บ<br />

หรือเครือข่ายทางสังคม


บทนา<br />

xiii<br />

บทที่ 9 “การทำงานร่วมกับข้อมูล” จะเกี่ยวกับเทคนิคและรูปแบบการจัดเก็บข้อมูลบนระบบ<br />

ปฏิบัติการแอนดรอยด์ รวมถึงการใช้งานฐานข้อมูล SQLite<br />

บทที่ 10 “การระบุตำแหน่ง” จะเกี่ยวข้องกับการติดต่อระบบ GPS และการเรียกใช้บริการ<br />

ต่างๆ ในการระบุตำแหน่งที่อยู่ปัจจุบัน<br />

บทที่ 11 “เทคนิคขั้นสูงสำหรับพัฒนาแอพบนแอนดรอยด์” จะเป็นการนำเทคนิคต่างๆ มา<br />

พัฒนาแอพ รวมทั้งการปรับแต่งแอพให้มีประสิทธิภาพในการทำงานยิ่งขึ้น<br />

บทที่ 12 “การตรวจสอบการทำงานของแอพ” จะพูดถึงการตรวจสอบการทำงาน และการหาข้อ<br />

ผิดพลาดภายในแอพที่พัฒนาด้วยการใช้เครื่องมือดีบั๊กแอพของแอนดรอยด์<br />

แหล่งข้อมูลอ้างอิงเพิ่มเติม<br />

แหล่งข้อมูลอ้างอิงเกี่ยวกับการพัฒนาแอพแอนดรอยด์มีอยู่มากมาย ซึ่งแหล่งที่น่าสนใจมีดังนี้<br />

m Android Source Code: http://source.android.com/<br />

m Android Developer Pages: http://developer.android.com/<br />

m Open Source Directory: http://osdir.com/<br />

m Stack Overflow Discussion Threads: http://stackoverflow.com/<br />

m Talk Android Developer Forums: http://www.talkandroid.com/android-forums/


xiv เกี่ยวกับผู้เขียน<br />

เกี่ยวกับผู้เขียน<br />

James Steele นักวิจัยระดับปริญญาเอกด้านฟิสิกส์ของสถาบันเทคโนโลยี Massachusetts<br />

และเป็นผู้คิดค้นนวัตกรรมใหม่ๆ ที่ถูกนำไปใช้ในการขับเคลื่อนธุรกิจที่ Silicon Valley มาแล้วมากมาย<br />

นับไม่ถ้วน<br />

Nelson To เจ้าของแอพพลิเคชั่นกว่า 10 แอพที่เผยแพร่ใน Android Market เขาได้ร่วมงาน<br />

กับองค์กรชั้นนำมากมาย อาทิ Think Computer, AOL, Standford University, Logitech (Google<br />

TV) และยังเปิดอบรมเกี่ยวกับ Android ในแถบ Bay Area และประเทศจีนด้วย


1<br />

บทที่ 1<br />

ก้าวแรกกับแอนดรอยด์<br />

ระบบปฏิบัติการแอนดรอยด์ (Android) ได้เริ่มเป็นที่รู้จักตั้งแต่ที่มีการประกาศก่อตั้งOpen Handset<br />

Alliance ในปี พ.ศ. 2550 โดยมีจุดมุ่งหมายเพื่อพัฒนาระบบปฏิบัติการแบบโอเพ่นซอร์ส(Open<br />

Source) สำหรับนำไปใช้ร่วมกับระบบการทำงานแบบฝังตัว (Embedded System) ซึ่งทาง Google ก็ได้<br />

สนับสนุนและผลักดันจนเป็นที่รู้จักกันอย่างแพร่หลายในอีก3 ปีต่อมา<br />

โทรศัพท์มือถือเป็นอุปกรณ์ชนิดหนึ่งที่มีการน ำเอาแอนดรอยด์มาใช้กัน นอกเหนือจากโทรศัพท์<br />

มือถือแล้ว ในอุปกรณ์อื่นๆ อย่างเช่นเน็ตบุ๊กหรือแท็บเล็ตก็มีการน ำระบบปฏิบัติการนี้มาใช้ด้วยเช่นกัน<br />

สำหรับเนื้อหาในบทนี้จะพูดถึงเกี่ยวกับแอนดรอยด์เบื้องต้นลักษณะการทำงาน ระบบฮาร์ดแวร์ที่<br />

สนับสนุน และรูปแบบการพัฒนาแอพ<br />

วิวัฒนาการของแอนดรอยด์<br />

Google ได้เล็งเห็นถึงอัตราการเติบโตของการใช้งานอินเตอร์เน็ตบนอุปกรณ์แบบพกพาจึงก่อตั้ง<br />

บริษัท Android,Inc. ขึ้นมาในปี พ.ศ. 2548 โดยมุ่งหมายที่จะพัฒนาระบบปฏิบัติการเพื่อใช้งานบน<br />

อุปกรณ์พกพา ส่วนทาง Apple ได้เปิดตัว iPhone ในปี พ.ศ. 2550 ด้วยแนวคิดของการนำจอภาพแบบ<br />

สัมผัสหลายจุดมาใช้งาน แถมยังเสนอตลาดเอาไว้ให้จำหน่ายหรือจับจ่ายซื้อแอพกันด้วย แอนดรอยด์ถูก<br />

พัฒนาอย่างรวดเร็วโดยได้นำคุณสมบัติเหล่านี้มารวมไว้ในระบบปฏิบัติการและท ำให้รองรับการทำงาน<br />

แบบมัลติทาสก์กิ้ง (Multitasking) ด้วย การทำงานร่วมกับระบบงานที่รองรับการทำงานระดับองค์กรเช่น<br />

ระบบอีเมล์ของ Microsoft Exchange, ระบบเครือข่ายส่วนตัวแบบเสมือน (Virtual Private Network-<br />

VPN) หรือการลบข้อมูลในอุปกรณ์พกพาจากระยะไกล ก็คล้ายๆ กับรูปแบบการทำงานของระบบปฏิบัติ<br />

การ BlackBerry ที่ทางบริษัท Research In Motion ได้นำมาใช้กับโทรศัพท์มือถือ BlackBerry ทุกรุ่น<br />

คุณสมบัติการรรองรับอุปกรณ์ที่หลากหลายและสามารถท ำงานร่วมกันได้นั้น ทำให้แอนดรอยด์ได้<br />

รับความนิยมอย่างสูง แต่ในขณะเดียวกันก็กลายเป็นปัญหาที่ค่อนข้างใหญ่ส ำหรับนักพัฒนาด้วย เพราะจะ<br />

ต้องพัฒนาแอพให้ใช้งานได้กับอุปกรณ์แทบทั้งหมด ซึ่งอุปกรณ์ที่มีในท้องตลาดนั้นมีความแตกต่างทั้ง<br />

เรื่องขนาดของหน้าจอ, ความละเอียดของหน้าจอ, รูปแบบของแป้นพิมพ์, อุปกรณ์ตรวจจับต่างๆ, อัตรา<br />

การรับส่งข้อมูล, ความเร็วในการประมวลผล เลยทำให้ผลลัพธ์ในการทำงานของแอพที่แสดงออกมาบน<br />

อุปกรณ์แต่ละรุ่นแตกต่างกันไป นอกจากนี้ยังคาดเดาความเร็วในการทำงานได้ยากด้วย และคงเป็นไปไม่<br />

ได้ถ้าเราจะนำแอพไปลองทดสอบกับอุปกรณ์ทุกรุ่น<br />

ระบบปฏิบัติการแอนดรอยด์พยายามแก้ไขปัญหาเหล่านี้ด้วยการท ำให้รูปแบบการพัฒนาแอพ<br />

สามารถทำงานได้บนหลายแพลตฟอร์ม และได้รับประสบการณ์การใช้งานที่ใกล้เคียงกัน โดยแยกเอาการ<br />

ทำงานของแอพออกจากรูปแบบการติดต่อกับฮาร์ดแวร์โดยตรง มาเป็นการติดต่อกับไลบรารีของระบบ<br />

ปฏิบัติการแทน ซึ่งจะทำให้มีความยืดหยุ่นในการปรับแต่งมากขึ้น ในกรณีที่มีการเปลี่ยนฮาร์ดแวร์เป็นรุ่น<br />

อื่นๆ ที่ใหม่ขึ้นแอพที่ใช้งานอยู่ในปัจจุบันก็จะยังท ำงานได้เป็นปกติ นี่เองคือแนวคิดในอุดมคติของการ<br />

พัฒนาแอพ


2 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

ดังนั้นในขั้นตอนของการพัฒนาแอพ สภาพแวดล้อมของระบบที่จะใช้ในการพัฒนาและทดสอบ<br />

การทำงานของแอพจึงเป็นสิ่งที่สำคัญ ทาง Google จึงได้นำเสนอปลั๊กอินที่ใช้ในการพัฒนาแอพที่มีชื่อ<br />

ว่า ADT (Android Development Tool) ซึ่งทำงานร่วมกับโปรแกรม Eclipse เลยทำให้ได้สภาพ<br />

แวดล้อมในการพัฒนาแอพที่สามารถจำลองการทำงานบนโปรเซสเซอร์ ARM ได้ และถ้าผู้ใช้งาน<br />

แอพนั้นๆ พบข้อผิดพลาดในการทำงาน แอพก็จะแจ้งข้อผิดพลาดดังกล่าวกลับไปยังผู้พัฒนาแอพผ่าน<br />

ทาง Android Market ได้<br />

ลักษณะของแอนดรอยด์<br />

ระบบปฏิบัติการแอนดรอยด์มีจุดเด่นที่น่าสนใจอยู่หลายจุด การทำความรู้จักกับจุดเด่นเหล่านี้จะ<br />

ทำให้คุณเข้าใจการทำงานของแอนดรอยด์มากขึ้น จะได้รู้ว่าอะไรที่ทำได้และทำไม่ได้บนระบบ<br />

ปฏิบัติการนี้<br />

แอนดรอยด์เป็นระบบปฏิบัติการที่ทำงานแบบฝังตัวโดยใช้โครงสร้างเดียวกับลีนุกซ์ (Linux)<br />

ซึ่งใช้ลีนุกซ์เคอร์เนล (Linux Kernel) เป็นแกนหลักในการทำงาน แต่การทำงานรอบข้างจะไม่ถูกฝังลง<br />

ในเคอร์เนล หรือพูดง่ายๆ ก็คือโครงสร้างมาตรฐานของลีนุกซ์จะไม่รองรับการทำงานของ X Windows<br />

และ GNU C ดังนั้นระบบปฏิบัติการแอนดรอยด์จึงใช้ประโยชน์จากจาวาเฟรมเวิร์ค แต่เฟรมเวิร์คที่<br />

ใช้นั้นจะไม่ใช่เฟรมเวิร์คมาตรฐาน ไม่สามารถใช้ Swing ได้ และไม่มีไลบรารี Timer ให้ใช้งานด้วย<br />

แอนดรอยด์จึงใช้ไลบรารีของตัวเองแทน ไลบรารีเหล่านี้ได้รับการปรับปรุงให้มีประสิทธิภาพการทำงาน<br />

ที่เหมาะสมกับการทำงานบนอุปกรณ์พกพา<br />

แอนดรอยด์มีลักษณะเป็นระบบปฏิบัติการแบบเปิด หมายความว่านักพัฒนาสามารถดูและใช้<br />

งานซอร์สโค้ดของระบบปฏิบัติการได้ รวมถึงการเข้าถึงเรดิโอสแต็ค (Radio Stack) เพื่อควบคุมการ<br />

ใช้งานระบบสื่อสารต่างๆ บนฮาร์ดแวร์ด้วย ซอร์สโค้ดพวกนี้ถือเป็นแหล่งข้อมูลอันดับต้นๆ สำหรับ<br />

ศึกษาการทำงานของแอนดรอยด์เลยก็ว่าได้ในกรณีที่ไม่สามารถหาเอกสารอ้างอิงการทำงาน นักพัฒนา<br />

จึงสามารถเขียนแอพให้ทำงานในแบบที่แอนดรอยด์ทำได้ และสามารถสร้างคอมโพเน็นต์ (Component)<br />

ที่มีการทำงานใกล้เคียงกับคอมโพเน็นต์ของระบบได้ด้วย แต่อย่างไรก็ตาม ในระบบปฏิบัติการ<br />

แอนดรอยด์ยังคงมีฮาร์ดแวร์และซอฟต์แวร์บางส่วนที่ไม่เปิดให้นักพัฒนาเข้าถึงได้โดยตรง อย่างเช่น<br />

การทำงานของระบบระบุพิกัด (GPS) เป็นต้น<br />

อีกจุดเด่นหนึ่งของระบบปฏิบัติการแอนดรอยด์ที่พัฒนาโดย Google อยู่ตรงที่ทาง Google<br />

ยังเป็นผู้พัฒนาระบบปฏิบัติการ Chrome ด้วย ซึ่งแอนดรอยด์ถูกออกแบบมาให้ทำงานแบบฝังตัวบน<br />

อุปกรณ์พกพา ส่วน Chrome ถูกสร้างขึ้นเพื่อรองรับโครงสร้างการทำงานแบบ Cloud (คลาวด์)<br />

โดยอุปกรณ์ที่เหมาะกับการทำงานแบบ Cloud คือกลุ่มเน็ตบุ๊ก นับเป็นตัวเลือกที่เหมาะสมที่อยู่กึ่งกลาง<br />

ระหว่างโทรศัพท์มือถือกับเครื่องแลปท็อปนั่นเอง และในตอนนี้แอนดรอยด์ก็กำลังพัฒนา Cloud ให้ใช้<br />

งานได้ครอบคลุมยิ่งขึ้นอยู่การพัฒนาแอพบน Cloud จะคล้ายกับการพัฒนาแอพบนแอนดรอยด์<br />

นั่นหมายความว่าเมื่อมีการใช้งานร่วมกับ Cloud จำนวนของผู้ที่ใช้แอนดรอยด์ก็จะยิ่งเพิ่มขึ้น<br />

อุปกรณ์ที่ทำงานร่วมกับแอนดรอยด์<br />

ในปัจจุบันนี้มีโทรศัพท์มือถือที่ใช้ระบบปฏิบัติการแอนดรอยด์วางขายอยู่ในตลาดมากกว่า 40 รุ่น<br />

จากผู้ผลิตกว่า 10 ราย แถมยังมีการนำแอนดรอยด์ไปใช้งานบนแท็บเล็ตและโทรทัศน์อีกด้วย<br />

แอพของแอนดรอยด์สามารถตรวจสอบชนิดและรุ่นของฮาร์ดแวร์ที่ตัวมันติดตั้งอยู่ได้โดยการเรียกใช้<br />

คลาสชื่อ android.os.Build ตามตัวอย่างดังนี้<br />

if(android.os.Build.MODEL.equals(“Nexus+One”)) { ... }


อุปกรณ์ที่ทำางานร่วมกับแอนดรอยด์<br />

ระบบปฏิบัติการแอนดรอยด์ที่ติดตั้งอยู่ในอุปกรณ์ต่างๆ นั้นมีโครงสร้างคล้ายๆ กัน แบ่งออกได้เป็น<br />

6 ส่วนดังนี้<br />

m Bootloader – เป็นส่วนที่ถูกเรียกใช้งานเมื่ออุปกรณ์เริ่มทำงาน<br />

m Boot Image – เป็นส่วนของเคอร์เนลและแรมดิสก์<br />

m System Image – เป็นส่วนที่จัดเก็บระบบปฏิบัติการแอนดรอยด์และแอพ<br />

m Data Image – เป็นส่วนที่จัดเก็บข้อมูลของระบบปฏิบัติการและแอพ<br />

m Recovery Image – เป็นส่วนที่จะถูกเรียกใช้งานเมื่อมีการกู้หรืออัพเกรดระบบปฏิบัติการ<br />

m Radio Image – เป็นส่วนที่เก็บข้อมูลของเรดิโอสแต็ค<br />

ส่วนประกอบทั้ง 6 ส่วนนี้จะถูกจัดเก็บลงในหน่วยความจำแฟลช ซึ่งข้อมูลเหล่านี้จะไม่สูญหาย<br />

แม้จะไม่มีกระแสไฟเลี้ยงระบบ ส่วนของหน่วยความจำแฟลชนี้บางครั้งจะทำงานเป็นหน่วยความจำที่<br />

ใช้อ่านเท่านั้น (ROM) แต่บางครั้งเราก็สามารถเขียนข้อมูลลงไปในหน่วยความจำนี้ได้<br />

เมื่ออุปกรณ์เริ่มทำงาน หน่วยประมวลผลจะสั่งให้ Bootloader ทำการโหลดเคอร์เนลและ<br />

แรมดิสก์เข้าสู่หน่วยความจำหลักเพื่อให้ทำงานได้รวดเร็ว หลังจากนั้นหน่วยประมวลผลจะทำงานตาม<br />

ชุดคำสั่งในเคอร์เนลตามลำดับ และสั่งงานให้ส่วนของ Radio Image เชื่อมต่อกับฮาร์ดแวร์<br />

ในตารางที่ 1.1 แสดงให้เห็นรายละเอียดในการเปรียบเทียบฮาร์ดแวร์ของโทรศัพท์รุ่นต่างๆ<br />

ทั้งในอดีตและปัจจุบัน โดยจะแสดงถึงสถาปัตยกรรมของฮาร์ดแวร์ที่ใช้ประมวลผล เช่น<br />

หน่วยประมวลผล (MPU) หน่วยความจำหลัก (SDRAM หรือ RAM) และส่วนของหน่วยความจำ<br />

แฟลช (ROM) ส่วนขนาดของหน้าจอนั้นจะแสดงหน่วยเป็นพิกเซล เพราะถ้าใช้เป็นหน่วยจุดต่อนิ้ว<br />

(dpi) อาจทำให้เกิดความคลาดเคลื่อนเวลาที่ขนาดของหน้าจอแตกต่างกัน ยกตัวอย่างเช่น ใน HTC<br />

Magic จะมีขนาดของจออยู่ที่ 3.2 นิ้ว และมีความละเอียดของจออยู่ที่ 320x480 พิกเซล เทียบเท่ากับ<br />

180 พิกเซลต่อนิ้ว แต่จะถูกจัดให้อยู่ในกลุ่มของความละเอียดหน้าจอระดับกลาง (ค่าเฉลี่ยในแอน<br />

ดรอยด์ กำหนดไว้ที่ 160 พิกเซล) เป็นต้น แม้ว่ามือถือสมาร์ทโฟนแต่ละรุ่นจะแตกต่างกันออกไป แต่<br />

แทบทุกรุ่นจะมีกล้องที่ใช้เซ็นเซอร์ CMOS เป็นตัวจับภาพ, มีบลูทูธ และมี Wi-Fi (802.11)<br />

3<br />

ตารางที่ 1.1 แสดงรายละเอียดของโทรศัพท์ที่ใช้ระบบปฏิบัติการแอนดรอยด์รุ่นต่างๆ อ้างอิง<br />

ข้อมูลจาก http://en.wikipedia.org/wiki/List_of_Android_devices และ http://pdadb.net/<br />

รุ่น หน่วยประมวลผล แรม/รอม จอภาพ คุณสมบัติอื่นๆ<br />

HTC Dream / G1<br />

(ตุลาคม 2551)<br />

528MHz QCOM<br />

MSM7201A<br />

192MB/256MB<br />

TFT LCD 320x480<br />

mdpi<br />

รองรับ GSM/UMTS,<br />

แป้นพิมพ์แบบสไลด์,<br />

แทร็คบอล บลูทูธ 2.0, Wi-Fi<br />

802.11b/g, กล้อง 3.1<br />

ล้านพิกเซล และ AGPS


4 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

ตารางที่ 1.1 ต่อ<br />

รุ่น หน่วยประมวลผล แรม/รอม จอภาพ คุณสมบัติอื่นๆ<br />

Samsung Moment<br />

(ตุลาคม 2552)<br />

800MHz<br />

ARM1176 JZF-S<br />

288MB/512MB<br />

AMOLED 320x480<br />

mdpi<br />

รองรับ CDMA/1xEV-DO,<br />

แป้นพิมพ์แบบสไลด์, บลู<br />

ทูธ 2.0, Wi-Fi 802.11b/g,<br />

กล้อง 3.1 ล้านพิกเซล และ<br />

AGPS<br />

Motorola Mile-<br />

Stone (Droid)<br />

(พฤศจิกายน 2552)<br />

550MHz TI<br />

OMAP3430<br />

256MB/512MB<br />

TFT LCD 480x854<br />

hdpi<br />

รองรับ GSM/UMTS หรือ<br />

CDMA/1xEV-DO, แป้นพิมพ์<br />

แบบสไลด์, บลูทูธ 2.1, Wi-Fi<br />

802.11b/g, กล้อง 5<br />

ล้านพิกเซล และ AGPS<br />

Nexus One /HTC<br />

Passion (มกราคม<br />

2553)<br />

1GHz QCOM<br />

Snapdragon<br />

512MB/512MB<br />

AMOLED 480x800<br />

hdpi<br />

รองรับ GSM/UMTS,<br />

แทร็คบอล, ไมโครโฟน 2<br />

ตัว, บลูทูธ 2.0, Wi-Fi<br />

802.11a/b/g/n, กล้อง 5<br />

ล้านพิกเซล, AGPS และ<br />

Geotagging<br />

HTC Droid<br />

Incredible (เมษายน<br />

2553)<br />

1GHz QCOM<br />

Snapdragon<br />

512MB/512MB<br />

AMOLED 480x800<br />

hdpi<br />

รองรับ CDMA/1xEV-DO,<br />

บลูทูธ 2, Wi-Fi 802.11a/b/<br />

g/n, กล้อง 8 ล้านพิกเซล,<br />

AGPS และ Geotagging<br />

HTC EVO 4G<br />

(มิถุนายน 2553)<br />

1GHz QCOM<br />

Snapdragon<br />

512MB/1GB<br />

TFT LCD 480x800<br />

hdpi<br />

รองรับ CDMA/1xEV-<br />

DO/802.16e-2005, บลู<br />

ทูธ 2.1, Wi-Fi 802.11b/g,<br />

กล้องด้านหลัง 8 ล้าน<br />

พิกเซล, กล้องด้านหน้า<br />

1.3 ล้านพิกเซล และ AGPS


ตารางที่ 1.1 ต่อ<br />

อุปกรณ์ที่ทำางานร่วมกับแอนดรอยด์<br />

5<br />

รุ่น หน่วยประมวลผล แรม/รอม จอภาพ คุณสมบัติอื่นๆ<br />

Motorola Droid<br />

X (กรกฎาคม<br />

2553)<br />

1 GHz TI<br />

OMAP3630<br />

512MB/8GB<br />

TFT LCD 480x854<br />

hdpi<br />

รองรับ CDMA/1xEV-DO,<br />

แป้นพิมพ์แบบสไลด์, บลูทูธ 2.1,<br />

Wi-Fi 802.11b/g/n, กล้อง<br />

8 ล้านพิกเซล, AGPS และ<br />

Geotagging<br />

Sony-Ericsson<br />

Xperia) (มิถุนา<br />

ยายน 2553)<br />

1 GHz QCOM<br />

Snapdragon<br />

256MB/1GB<br />

TFT LCD 480x854<br />

hdpi<br />

รองรับ GSM/UMTS, บลูทูธ<br />

2.1, Wi-Fi 802.11b/g, กล้อง<br />

8 ล้านพิกเซล, AGPS และ<br />

Geotagging<br />

Samsung Ga;axy<br />

S Pro (สิงหาา<br />

คม 2553)<br />

1 GHz Samsung<br />

Hummingbord<br />

512MB/2GB<br />

AMOLED 480x800<br />

hdpi<br />

รองรับ CDMA/1xEV-DO, วิทยุ<br />

FM, แป้นพิมพ์แบบสไลด์,<br />

บลูทูธ 3.0, Wi-Fi<br />

802.11b/g/n, กล้องด้านหลัง<br />

5 ล้านพิกเซล, กล้องด้านหน้า<br />

0.3 ล้านพิกเซล และ AGPS<br />

Acer Stream /<br />

Liquid (กันยายน<br />

2553)<br />

1 GHz QCOM<br />

Snapdragon<br />

512MB/512MB<br />

AMOLED 480x800<br />

hdpi<br />

รองรับ GSM/UMTS, วิทยุ<br />

FM, บลูทูธ 2.1, Wi-Fi<br />

802.11b/g/n, กล้อง 8 ล้าน<br />

พิกเซล, AGPS และ Geotagging<br />

นอกเหนือจากการเพิ่มขนาดของหน่วยความจำและประสิทธิภาพในการประมวลผลในโทรศัพท์<br />

รุ่นใหม่ๆ แล้ว อีกหนึ่งจุดเด่นอยู่ที่การเพิ่มเติมคุณสมบัติใหม่ๆ เข้ามานั่นเอง ในบางรุ่นนั้นรองรับการ<br />

ทำงานบนเครือข่าย 4G ฟังวิทยุ FM ได้ มีแป้นพิมพ์แบบสไลด์ รวมทั้งมีกล้องถ่ายรูปทั้งด้านหน้าและ<br />

ด้านหลังด้วย การศึกษารายละเอียดของฮาร์ดแวร์เหล่านี้ช่วยให้นักพัฒนาสามารถสร้างแอพให้มี<br />

ประสิทธิภาพได้ เท่านั้นยังไม่พอ ในโทรศัพท์รุ่นใหม่ๆ ยังได้ติดตั้งส่วนที่อ่านข้อมูลจากหน่วยความจำ<br />

ภายนอก (SD Card) เอาไว้ด้วย ทำให้มีเนื้อที่ในการเก็บข้อมูลมากขึ้น แต่อย่างไรก็ตาม ในแอนดรอยด์<br />

ที่ต่ำกว่าเวอร์ชั่น 2.2 จะรองรับการเก็บแอพลงบนหน่วยความจำ ROM ภายในเครื่องเท่านั้น ไม่รองรับ<br />

หน่วยความจำภายนอก


6 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

ฮาร์ดแวร์ของ HTC<br />

HTC เป็นบริษัทของไต้หวัน ก่อตั้งขึ้นในปี พ.ศ. 2550 อุปกรณ์ที่ใช้งานร่วมกับแอนดรอยด์รุ่น<br />

แรกของ HTC คือรุ่น HTC Dream (บางครั้งจะเรียกว่ารุ่น G1 ซึ่งอักษร G จะย่อมาจาก Google)<br />

วางขายช่วงเดือนตุลาคม ปี พ.ศ. 2551 จากนั้นก็ได้ออกรุ่นอื่นๆ ตามมาอีกหลายรุ่น รวมถึงรุ่น Nexus<br />

One ของ Google ด้วย<br />

Nexus One เป็นโทรศัพท์มือถือแอนดรอยด์รุ่นแรกที่ใช้หน่วยประมวลผลของ Qualcomm<br />

ความเร็ว 1GHz ที่มีชื่อว่า Snapdragon ซึ่งมีโครงสร้างที่แตกต่างจากหน่วยประมวลผล ARM<br />

ในหน่วยประมวลผล Snapdragon นั้นมีการติดตั้งหน่วยประมวลผลวิดีโอความละเอียดสูง (720p)<br />

และใน Nexus One มีการติดตั้งไมโครโฟนเอาไว้ 2 ตัวเพื่อไว้ทำหน้าที่ตัดเสียงรบกวนในระหว่างการ<br />

สนทนา และมีแทร็คบอลที่สามารถแสดงไฟได้หลายสีที่แสดงผลแยกตามข้อมูลแจ้งเตือนต่างๆ ได้<br />

ในเดือนเมษายน ปี พ.ศ. 2553 HTC ได้เปิดตัว Droid Incredible โดยคุณสามารถดูราย<br />

ละเอียดได้จากตารางที่ 1.1 โทรศัพท์รุ่นนี้มีลักษณะใกล้เคียงกับรุ่น Nexus One แต่จะรองรับเครือข่าย<br />

CDMA และมีกล้องถ่ายรูปที่มีความละเอียดสูงกว่า จากนั้นเดือนมิถุนายนในปีเดียวกันก็ได้เปิดตัว<br />

HTC EVO 4G ซึ่งเป็นโทรศัพท์แอนดรอยด์รุ่นแรกที่รองรับการทำงาน WiMAX (802.16e)<br />

ฮาร์ดแวร์ของ Motorola<br />

Motorola ได้ผลิตโทรศัพท์มือถือเครื่องแรกในปี พ.ศ. 2523 และประสบความสำเร็จอย่างสูงใน<br />

ช่วงนั้น แต่ในแง่ของแอนดรอยด์ ทาง Motorola เพิ่งเข้าสู่แพลตฟอร์มนี้ได้ไม่นานนัก โดย Motorola<br />

ได้ออกรุ่น Droid ในเดือนพฤศจิกายน ปี พ.ศ. 2552 ซึ่งรองรับการใช้งานระบบ CDMA และออกรุ่น<br />

Milestone ที่รองรับการใช้งานระบบ GSM<br />

นอกจากนั้นในปัจจุบัน Motorola ยังถือว่าเป็นแบรนด์ที่ติดอันดับ 1 ใน 10 ของผู้ผลิตโทรศัพท์<br />

มือถือที่ใช้แอนดรอยด์ด้วย สำหรับรุ่น Droid X นั้นจะมีคุณสมบัติที่ใกล้เคียงกับ HTC รุ่น Droid<br />

Incredible ซึ่งมีกล้องวิดีโอความละเอียดสูง<br />

ฮาร์ดแวร์ของ Samsung<br />

Samsung ถือเป็นผู้ผลิตโทรศัพท์มือถือรายใหญ่ในปัจจุบันและมีการนำระบบปฏิบัติการแอนดรอยด์<br />

มาใช้กับโทรศัพท์มือถือที่ผลิตขึ้นด้วย โดยรุ่นแรกของ Samsung คือรุ่น Moment ออกสู่ตลาดใน<br />

เดือนพฤศจิกายน ปี พ.ศ. 2552 ในรุ่นแรกนี้ยังไม่มีคุณสมบัติจอสัมผัสแบบหลายจุด และยังไม่<br />

สามารถอัพเกรดไปสู่แอนดรอยด์เวอร์ชั่น 2.1 ได้ ซึ่งในรุ่นนี้ยังมีเวอร์ชั่นที่ออกแบบมาเป็นพิเศษ<br />

สามารถรับสัญญาณโทรทัศน์ได้ด้วย และมีขายในบางประเทศเท่านั้น<br />

Samsung Galaxy S เป็นโทรศัพท์มือถือแอนดรอยด์ที่ทาง Samsung หมายมั่นปั้นมือให้มา<br />

ช่วงชิงส่วนแบ่งทางการตลาดของ iPhone โดยเฉพาะ ซึ่ง iPhone รุ่น 3G และ 3GS เองก็ใช้หน่วย<br />

ประมวลผลที่พัฒนาโดย Samsung เช่นกัน Samsung Galaxy S นั้นใช้หน่วยประมวลผลของ<br />

Samsung ชื่อว่า Hummingbird มีความเร็ว 1GHz และมีสถาปัตยกรรมภายในเป็น ARM Cortex-8<br />

และเป็นโทรศัพท์มือถือรุ่นแรกๆ ที่มีการติดตั้งบลูทูธเวอร์ชั่น 3.0 ด้วย


แท็บเล็ต<br />

ในช่วงที่ Apple ดัน iPad เข้าสู่ตลาดนั้น ทางผู้ผลิตอุปกรณ์ที่ใช้แอนดรอยด์ก็ได้พัฒนา<br />

แท็บเล็ตเช่นกัน ลักษณะเด่นของแท็บเล็ตอยู่ที่จอภาพขนาดใหญ่ มีตั้งแต่ 4.8 นิ้วขึ้นไป และรองรับ<br />

Wi-Fi ในแท็บเล็ตบางรุ่นใช้เครือข่าย 3G ได้ด้วย จึงทำให้แท็บเล็ตมีลักษณะคล้ายกับโทรศัพท์<br />

สมาร์ทโฟนที่มีจอภาพขนาดใหญ่นั่นเอง<br />

บริษัท Archos คือผู้ผลิตรายแรกที่พัฒนาแท็บเล็ตที่ใช้แอนดรอยด์และวางจำหน่ายในช่วง<br />

ปลายปี พ.ศ. 2552 ชื่อรุ่นว่า Archos 5 มีจอภาพขนาด 4.8 นิ้ว ต่อมาก็ได้เปิดตัวรุ่น Archos 7 ซึ่งมี<br />

จอภาพขนาด 7 นิ้ว โดยในรุ่นนี้ได้ติดตั้งฮาร์ดดิสก์เพื่อเพิ่มขนาดความจุข้อมูลด้วย ในขณะเดียวกับ<br />

ทางบริษัท Dell ก็จำหน่ายแท็บเล็ตรุ่น Streak มีจอภาพขนาด 5 นิ้ว และมีแผนที่จะออกรุ่นที่มี<br />

จอภาพขนาด 7 และ 10 นิ้ว ส่วนทาง Samsung ออกแท็บเล็ตที่มีจอภาพขนาด 7 นิ้ว ชื่อ Galaxy<br />

Tab คุณสามารถดูข้อมูลเปรียบเทียบของแท็บเล็ตแต่ละรุ่นได้ในตารางที่ 1.2<br />

ตารางที่ 1.2 เปรียบเทียบขีดความสามารถของแท็บเล็ตรุ่นต่างๆ<br />

อุปกรณ์ที่ทำางานร่วมกับแอนดรอยด์<br />

รุ่น หน่วยประมวลผล แรม/รอม จอภาพ คุณสมบัติอื่นๆ<br />

7<br />

Archos 5<br />

(กันยายน 2552)<br />

800MHz TI OMAP<br />

3440<br />

256MB/8GB TFT LCD 4.8 นิ้ว<br />

840x480<br />

บลูทูธ 2.0, Wi-Fi<br />

802.11b/g/n, วิทยุ FM<br />

Archos 7<br />

(มิถุนายน 2553)<br />

600MHz Rockchip<br />

RK2808<br />

128MB/8GB TFT LCD 7 นิ้ว<br />

800x480<br />

Wi-Fi 802.11b/g<br />

Dell Streak<br />

(มิถุนายน 2553)<br />

1GHz QCOM Snapdragon<br />

256MB/512MB TFT LCD 5 นิ้ว<br />

800x480<br />

รองรับ GSM/UMTS, บลูทูธ<br />

2.1, Wi-Fi 802.11b/g, กล้อง<br />

ด้านหลัง 5 ล้านพิกเซล, กล้อง<br />

ด้านหลัง 0.3 ล้านพิกเซล,<br />

AGPS และ Geotagging<br />

Samsung Galaxy<br />

Tablet GT-P1000<br />

(กันยายน 2553)<br />

1GHz Samsung<br />

Hummingbird<br />

512MB/16GB TFT LCD 7 นิ้ว<br />

1024x600<br />

รองรับ GSM/UMTS,<br />

บลูทูธ 3.0, Wi-Fi<br />

802.11b/g/n, กล้องถ่ายภาพ<br />

3.1 ล้านพิกเซล<br />

อุปกรณ์อื่นๆ<br />

เนื่องจากโครงสร้างของแอนดรอยด์เหมาะที่จะใช้งานกับอุปกรณ์ประเภทที่มีระบบปฏิบัติการ<br />

แบบฝังตัว แอนดรอยด์จึงถูกนำไปใช้ในอุตสาหกรรมต่างๆ ด้วย นอกเหนือจากโทรศัพท์มือถือและ<br />

แท็บเล็ต อย่างเช่นบริษัท Shanghai Automotive Industry Corporation ก็ได้ผลิตรถยนต์รุ่น<br />

Roewe 350 ออกมา ซึ่งเป็นรถยนต์รุ่นแรกที่ใช้แอนดรอยด์ในส่วนของระบบนำทางด้วย GPS และใช้<br />

เป็นเว็บบราวเซอร์


8 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

GoogleTV เป็นโทรทัศน์รุ่นแรกที่ใช้แอนดรอยด์ โดยเป็นการพัฒนาร่วมกันระหว่างบริษัทต่างๆ<br />

ซึ่ง Google เป็นผู้พัฒนาซอฟต์แวร์ Sony เป็นผู้ผลิตโทรทัศน์ Intel เป็นผู้ผลิตหน่วยประมวลผล<br />

และ Logitech เป็นผู้พัฒนาในส่วนของการควบคุม จึงทำให้ GoogleTV เป็นโทรทัศน์ที่เชื่อมต่อ<br />

อินเตอร์เน็ตได้ และสามารถติดตั้งแอพเพิ่มเติมโดยดาวน์โหลดจาก Android Market ได้ด้วย<br />

ความแตกต่างของฮาร์ดแวร์ที่ใช้งานร่วมกับแอนดรอยด์<br />

จากข้อมูลในตารางที่ 1.1 จะเห็นว่าแอนดรอยด์ทำงานบนฮาร์ดแวร์ที่มีความหลากหลาย ซึ่งผู้<br />

พัฒนาไม่จำเป็นจะต้องสนใจในรายละเอียดของฮาร์ดแวร์แต่ละรุ่นมากนักตามแนวคิดของการพัฒนา<br />

โปรแกรมบนแอนดรอยด์ แต่อย่างไรก็ตาม ในความเป็นจริงการพัฒนาแอพก็ยังคงต้องให้ใส่ใจในราย<br />

ละเอียดของฮาร์ดแวร์บางส่วนอยู่ดี ไม่ว่าจะเป็น จอภาพ การรับข้อมูลจากผู้ใช้ หรือการใช้ตัวตรวจจับ<br />

ต่างๆ<br />

จอภาพ<br />

ในปัจจุบันเทคโนโลยีที่ใช้ในการผลิตจอแสดงผลมีอยู่ 2 แบบ คือ แบบจอผลึกเหลว (LCD)<br />

และแบบไดโอดเปล่งแสง (LED) ซึ่งในอุปกรณ์แอนดรอยด์จะใช้จอภาพแบบ TFT (LCD) และแบบ<br />

AMOLED (LED) โดยจอภาพแบบ TFT จะมีอายุใช้งานที่ยาวนานกว่า ส่วนจอภาพแบบ AMOLED<br />

มีดีตรงที่ไม่ต้องใช้แสงแบ็คไลท์ในการทำงาน จึงใช้พลังงานน้อยและแสดงสีสันได้ดีกว่า โดยเฉพาะ<br />

ความเข้มของสีดำที่จะไม่ถูกรบกวนจากแสงของแบ็คไลท์<br />

โดยทั่วไปแล้วอุปกรณ์แอนดรอยด์จะถูกแบ่งกลุ่มออกเป็นจอภาพขนาดเล็ก ขนาดปกติ<br />

และขนาดใหญ่ รวมทั้งมีการแยกกลุ่มเป็นความละเอียดจอภาพแบบสูง กลาง และต่ำด้วย ในตารางที่<br />

1.3 จะแสดงถึงขนาดของจอภาพที่มีการใช้งานในระบบปฏิบัติการแอนดรอยด์<br />

ตารางที่ 1.3 ขนาดของจอภาพที่รองรับระบบปฏิบัติการแอนดรอยด์<br />

ขนาดจอภาพ ความละเอียดระดับต่ำ<br />

(~120 ppi), ldpi<br />

จอภาพขนาดเล็ก<br />

จอภาพขนาดปกติ<br />

จอภาพขนาดใหญ่<br />

QVGA (240x320) ขนาด<br />

2.6–3.0 นิ้ว<br />

WQVGA (240x400) ขนาด<br />

3.2–3.5 นิ้ว<br />

FWVGA (240x432) ขนาด<br />

3.5–3.8 นิ้ว<br />

ความละเอียดระดับกลาง<br />

(~160 ppi), mdpi<br />

HVGA (320x480) ขนาด<br />

3.0–3.5 นิ้ว<br />

WVGA (480x800) ขนาด<br />

4.8–5.5 นิ้ว<br />

FWVGA (480x854) ขนาด<br />

5.0–5.8 นิ้ว<br />

ความละเอียดระดับสูง<br />

(~240 ppi), hdpi<br />

WVGA (480x800) ขนาด<br />

3.3–4.0 นิ้ว<br />

FWVGA (480x854) ขนาด<br />

3.5–4.0 นิ้ว


ความแตกต่างของฮารด์แวร์ที่ใช้งานร่วมกับแอนดรอยด์<br />

วิธีการรับข้อมูล<br />

เทคโนโลยีจอภาพแบบสัมผัส ทำให้ผู้ใช้สามารถโต้ตอบกับจอแสดงผลได้โดยตรง ในปัจจุบันมี<br />

การใช้เทคโนโลยีของจอสัมผัส ดังนี้<br />

m Resistive – ใช้แผ่นตัวต้านทานจำนวน 2 แผ่นติดตั้งที่ชั้นบนสุดของจอภาพ เมื่อมีวัตถุ<br />

กดทับ เช่น นิ้วมือ ปากกา กดลงบนพื้นผิว จะทำให้แผ่นตัวต้านทานทั้ง 2 สัมผัสกัน และ<br />

แสดงค่าความต้านทานระดับต่างๆ ออกมา โดยค่าที่ได้จะถูกนำไปประมวลผลเพื่อระบุ<br />

พิกัดบนจอภาพ เทคโนโลยีนี้มีราคาไม่สูง แต่มีข้อจำกัดตรงที่แผ่นตัวต้านทานมีความ<br />

โปร่งแสงเพียง 75% เลยทำให้จอภาพแสดงสีได้ไม่คมชัด และยังไม่รองรับการทำงาน<br />

แบบสัมผัสหลายจุดพร้อมๆ กันด้วย<br />

m Capasitive – มีลักษณะเป็นแผ่นตัวเก็บประจุติดตั้งบนจอภาพ เมื่อมีนิ้วมือหรือวัตถุตัวนำ<br />

ใดๆ มาสัมผัสลงบนแผ่นตัวเก็บประจุนี้ จะทำให้ค่าประจุที่ตำแหน่งนั้นๆ เปลี่ยนไป แล้วนำ<br />

ค่านั้นมาแปลงเป็นพิกัดบนจอภาพ เทคโนโลยีนี้มีจุดเด่นตรงที่แผ่นตัวเก็บประจุมีความ<br />

โปร่งแสงถึง 90% แต่ความแม่นยำจะต่ำกว่าจอภาพแบบ Resistive เล็กน้อย<br />

m Surface Acoustic Wave – จอภาพชนิดนี้ใช้เทคโนโลยีที่ค่อนข้างสูงกว่าแบบอื่นๆ<br />

โดยจะใช้คลื่นอัลตราโซนิคในการรับส่งสัญญาณ เมื่อมีนิ้วมือหรือวัตถุใดๆ มาสัมผัสบน<br />

จอภาพ คลื่นอัลตราโซนิคที่ตำแหน่งนั้นจะถูกดูดซับ และนำค่านั้นมาแปลงเป็นพิกัดบน<br />

จอภาพ จุดเด่นของจอภาพชนิดนี้คือมีความทนทาน แต่เหมาะที่จะใช้กับจอภาพขนาดใหญ่<br />

เท่านั้น เช่น จอภาพของตู้เอทีเอ็ม เป็นต้น<br />

ในอุปกรณ์แอนดรอยด์มีการนำเทคโนโลยีจอภาพทั้งแบบ Resistive และแบบ Capasitive มา<br />

ใช้ โดยในรุ่นที่ใช้จอภาพแบบ Capasitive สามารถรองรับการทำงานแบบสัมผัสหลายจุดพร้อมกันได้<br />

ด้วย<br />

นอกจากจอภาพแบบสัมผัสแล้ว ในอุปกรณ์แอนดรอยด์ยังมีการติดตั้งอุปกรณ์เพิ่มเติมเพื่อช่วย<br />

ในการใช้งานกับจอภาพด้วย เช่น<br />

m D-pad (Direction pad) – มีลักษณะเป็นปุ่มบน ล่าง ซ้าย ขวา<br />

m Trackball – มีลักษณะเป็นลูกกลิ้ง คล้ายกับการทำงานของเมาส์<br />

m Trackpad – มีลักษณะเป็นพื้นที่สี่เหลี่ยมเล็กๆ ใช้สัมผัสเพื่อระบุตำแหน่ง<br />

เซ็นเซอร์ตรวจจับ<br />

โทรศัพท์มือถือในปัจจุบันนอกจากจะมีลำโพงและไมโครโฟนซึ่งเป็นอุปกรณ์พื้นฐานแล้ว ยังมีการ<br />

ติดตั้งตัวตรวจจับต่างๆ ไว้หลายอย่าง ช่วยให้เกิดประโยชน์ในการใช้งานเพิ่มขึ้น รวมถึงกล้องที่มีความ<br />

ละเอียดหลายระดับ ทำให้ผู้ใช้สามารถเลือกแบบที่ตรงกับความต้องการได้อย่างอิสระ<br />

ในโทรศัพท์มือถือประเภทสมาร์ทโฟนจะมีการติดตั้งตัวตรวจจับมาตรฐานเอาไว้ 3 ชนิด คือ<br />

ตัวตรวจจับอัตราเร่งแบบ 3 แกนเพื่อใช้จับทิศทางที่กระทาจากแรงโน้มถ่วง ตัวตรวจจับสนามแม่เหล็ก<br />

เพื่อใช้จับทิศทางที่อ้างอิงจากสนามแม่เหล็กโลก และตัวตรวจจับอุณหภูมิ ตัวอย่างเช่น HTC Dream<br />

(G1) ก็เป็นโทรศัพท์รุ่นหนึ่งที่มีการติดตั้งตัวตรวจจับแบบที่ว่ามา (การตรวจสอบว่าโทรศัพท์มีตัวตรวจ<br />

จับอะไรติดตั้งไว้บ้าง ให้ใช้คาสั่ง getSensorList() โดยคุณสามารถดูรายละเอียดเพิ่มเติมได้ในบทที่<br />

7 “การติดต่อกับฮารด์แวร์ต่างๆ”)<br />

9


10 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

AK8976A 3-axis Accelerometer<br />

AK8976A 3-axis Magnetic field sensor<br />

AK8976A Orientation sensor<br />

AK8976A Temperature sensor<br />

โมดูล AK8976A เป็นโมดูลที่พัฒนาโดยบริษัท Asahi Kasei Microsystem (AKM) เป็นโมดูล<br />

ที่รวมเอาตัวตรวจจับทั้ง 3 เข้าไว้ด้วยกัน อันประกอบไปด้วยตัวตรวจจับอัตราเร่งแบบเพียโซอิเลกทริก<br />

(Piezoresistive) ตัวตรวจจับสนามแม่เหล็ก และตัววัดอุณหภูมิ โดยข้อมูลที่ได้จากโมดูลจะมีขนาด<br />

8 บิท ส่วนการตรวจจับการหมุนหรือตะแคงจอภาพนั้น จะเป็นการทำงานร่วมกันระหว่างตัวตรวจจับ<br />

อัตราเร่ง และตัวตรวจจับสนามแม่เหล็ก<br />

รายละเอียดด้านล่างนี้เป็นข้อมูลตัวตรวจจับที่ได้จาก Motorola Droid<br />

LIS331DLH 3-axis Accelerometer<br />

AK8973 3-axis Magnetic field sensor<br />

AK8973 Temperature sensor<br />

SFH7743 Proximity sensor<br />

Orientation sensor type<br />

LM3530 Light sensor<br />

โมดูล LIS331DLH เป็นโมดูลขนาด 12 บิทที่พัฒนาโดยบริษัท ST Microelelctronic มีความ<br />

แม่นยำสูงเพราะสามารถประมวลผลสัญญาณได้ที่ 1KHz ส่วนโมดูล AK8673 จะเป็นโมดูลที่ติดตั้งตัว<br />

ตรวจจับสนามแม่เหล็กและตัววัดอุณหภูมิเท่านั้น<br />

จากรายละเอียดข้างต้นจะเห็นว่า Motorola Droid มีการติดตั้งตัวตรวจจับเพิ่มเติมอีก 2 ชนิด<br />

คือ โมดูล SFH7743 ที่ใช้ในการตรวจจับแสง ซึ่งจะมีประโยชน์ในการเปิดปิดจอภาพ เมื่อมีการเข้าใกล้<br />

วัตถุใดๆ ในระยะ 40 มม. (เช่น การปิดจอภาพเมื่อนำโทรศัพท์มาไว้ใกล้ๆ หู) และโมดูล LM3530<br />

เป็นตัวตรวจจับสภาพแสงซึ่งพัฒนาโดยบริษัท National Semiconductor โมดูลนี้จะใช้ในการตรวจวัด<br />

สภาพแสงภายนอกเพื่อนำมาปรับความสว่างของจอภาพแบบอัตโนมัติ<br />

อีกตัวอย่างหนึ่งเป็นการแสดงรายละเอียดของตัวตรวจจับจาก HTC รุ่น EVO 4G<br />

BMA150 3-axis Accelerometer<br />

AK8973 3-axis Magnetic field sensor<br />

AK8973 Orientation sensor<br />

CM3602 Proximity sensor<br />

CM3602 Light sensor<br />

โมดูล BMA150 จากบริษัท Bosch Sensortec เป็นโมดูลตรวจจับอัตราเร่ง ซึ่งสามารถ<br />

ประมวลผลสัญญาณได้สูงถึง 1.5KHz และโมดูล CM3602 จากบริษัท Capella Microsystem<br />

เป็นโมดูลที่รวมเอาตัวตรวจจับแสง และตัวตรวจจับวัตถุเข้าด้วยกัน<br />

จะเห็นได้ว่าโทรศัพท์แอนดรอยด์แต่ละรุ่นนั้นมีรายละเอียดของฮาร์ดแวร์ภายในที่หลากหลาย<br />

จึงทำให้ประสิทธิภาพในการประมวลผลและความแม่นยำของตัวตรวจจับแตกต่างกัน<br />

คุณสมบัติของแอนดรอยด์<br />

รายละเอียดและคุณสมบัติของระบบปฏิบัติการแอนดรอยด์นั้นถือเป็นเนื้อหาหลักที่เราจะพูดถึง<br />

ในหนังสือเล่มนี้อยู่แล้ว ในทางการตลาดนั้น คุณสมบัติบางจุดถือว่าเป็นจุดขายและใช้สร้างความ<br />

แตกต่างให้ผลิตภัณฑ์ได้ ดังนั้นจึงเป็นเรื่องที่ดีที่เราจะมาศึกษาถึงจุดเด่นของระบบปฏิบัติการนี้และ<br />

นำจุดเด่นนั้นๆ มาใช้ประโยชน์ให้ได้มากที่สุด


การพัฒนาแอนดรอยด์<br />

มัลติโปรเซส (Multiprocess) และแอพวิดเจ็ต (App Widget)<br />

ระบบปฏิบัติการแอนดรอยด์จะทำงานร่วมกับหน่วยประมวลผลแบบหลายๆ งานพร้อมกัน<br />

โดยจะมีการจัดลำดับความสำคัญในการประมวลผลของแต่ละแอพ จึงสามารถกำหนดให้งานบางอย่าง<br />

ทำงานแบบเบื้องหน้าหรือเบื้องหลังได้ อย่างเช่นว่าในขณะที่ผู้ใช้กำลังเล่นเกมอยู่นั้น แอพที่ทำงานอยู่<br />

เบื้องหลังก็สามารถตรวจสอบราคาหุ้นและแจ้งเตือนผู้ใช้ไปพร้อมๆ กันได้ ซึ่งลักษณะการทำงานแบบนี้<br />

เรียกว่า “มัลติโปรเซส” (Multiprocess)<br />

แอพวิดเจ็ต (App Widget) เป็นแอพขนาดเล็กที่ทำงานร่วมกับแอพอื่นๆ ได้ ตัวอย่างเช่น<br />

การทำงานร่วมกับหน้าจอหลัก (Home Screen) แอพวิดเจ็ตสามารถประมวลผลต่างๆ ได้ เช่น<br />

การเล่นไฟล์เพลง หรือการตรวจสอบสภาพอากาศในขณะที่แอพอื่นๆ กำลังทำงานอยู่<br />

มัลติโปรเซสจะช่วยให้การทำงานของผู้ใช้มีประสิทธิภาพยิ่งขึ้น แต่ยังคงมีสิ่งที่ต้องคำนึงถึงอยู่<br />

นั่นคือการทำงานในลักษณะนี้จะใช้พลังงานมากกว่าปกติ จึงอาจจะทำให้ระยะเวลาในการใช้แบตเตอรี่<br />

สั้นลง สำหรับรายละเอียดเกี่ยวกับมัลติโปรเซส ดูได้ในบทที่ 3 “เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้ง<br />

เตือน”<br />

ทัช มัลติทัช และเจสเจอร์<br />

ระบบการใช้งานแบบจอสัมผัสเป็นวิธีการรับข้อมูลที่เหมาะจะนำมาใช้กับอุปกรณ์ประเภท<br />

โทรศัพท์มือถือมาก โดยปกติเราจะทำงานกับจอสัมผัสด้วยการใช้ปลายนิ้วมือกดและลากไปยังจุดต่างๆ<br />

บนจอภาพตามที่ต้องการ ซึ่งระบบมัลติทัช (Multitouch) หรือการสัมผัสจอหลายๆ จุดพร้อมกันก็ใช้กับ<br />

ระบบปฏิบัติการแอนดรอยด์ได้เช่นกัน โดยจะใช้ในเรื่องของการซูมขยาย หรือปรับเปลี่ยนมุมมองต่างๆ<br />

สำหรับการสัมผัสจอภาพในบางรูปแบบนั้น ผู้พัฒนาสามารถเขียนโปรแกรมเพิ่มเติมเพื่อให้ตอบ<br />

สนองการทำงานบางอย่างได้ ซึ่งการทำงานในลักษณะนี้เรียกกันว่า “เจสเจอร์” (Gesture) คุณสามารถ<br />

ดูวิธีการสร้างเจสเจอร์ขึ้นเองเพื่อใช้ในแอพที่พัฒนาขึ้นได้ในบทที่ 5 “อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการ<br />

ติดต่อกับผู้ใช้งาน”<br />

แป้นพิมพ์แบบฮาร์ดแวร์และซอฟต์แวร์<br />

คุณสมบัติหนึ่งที่ถือเป็นพื้นฐานของโทรศัพท์มือถือประเภทสมาร์ทโฟนก็คือ แป้นพิมพ์<br />

(Keyboard) ซึ่งมีทั้งแบบฮาร์ดแวร์และซอฟต์แวร์ เวลาใช้งานจริงคุณจะรู้สึกว่าแป้นพิมพ์แบบ<br />

ฮาร์ดแวร์ที่เป็นปุ่มจริงๆ สัมผัสได้โดยตรงบนตัวเครื่องจะสามารถพิมพ์ข้อความได้เร็วและสะดวกสบาย<br />

กว่าแป้นพิมพ์แบบซอฟต์แวร์ เพราะแม้ว่าแป้นพิมพ์แบบซอฟต์แวร์จะมีขนาดกะทัดรัดและมีความ<br />

สวยงามกว่า แต่มีข้อเสียตรงที่มันจะกินพื้นที่บนจอภาพไปส่วนหนึ่งเพื่อแสดงแป้นพิมพ์นั่นเอง<br />

ด้วยเหตุนี้จึงเป็นเรื่องสำคัญของผู้พัฒนาที่จะต้องพัฒนาโปรแกรมเพื่อรองรับการทำงานร่วมกับแป้น<br />

พิมพ์ทั้ง 2 แบบนี้ รวมทั้งสร้างส่วนการติดต่อกับผู้ใช้งานให้มีความเหมาะสมด้วย<br />

การพัฒนาแอนดรอยด์<br />

หนังสือเล่มนี้จะกล่าวถึงการเขียนโปรแกรมบนระบบปฏิบัติการแอนดรอยด์ ซึ่งเป็นเนื้อหาหลัก<br />

ของหนังสือเล่มนี้ อย่างไรก็ดี ยังมีข้อตกลงบางอย่างที่จะต้องทำความเข้าใจก่อนที่จะเริ่มพัฒนาแอพ<br />

โดยอ้างอิงตามเนื้อหาในหนังสือเล่มนี้<br />

11


12 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

การใช้โค้ดหรือเทคนิคต่างๆ ในหนังสือเล่มนี้<br />

โค้ดหรือชุดคำสั่งในหนังสือเล่มนี้ส่วนใหญ่ทำงานอย่างอิสระ และมีข้อมูลทั้งหมดที่จำเป็นในการ<br />

สั่งให้แอพทำงานบนอุปกรณ์แอนดรอยด์ได้ อย่างในบทที่ 2 “การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ<br />

อินเท็นต์” มีไฟล์หลายไฟล์ที่ถูกสร้างขึ้นมาและจำเป็นต้องใช้เพื่อให้แอพทำงานได้ หากมีส่วนใดส่วน<br />

หนึ่งขาดหายไปก็จะทำงานไม่ได้ ดังนั้นทุกๆ ชุดคำสั่งจึงประกอบไปด้วยไฟล์ที่จำเป็น และแต่ละไฟล์จะ<br />

ถูกแสดงเอาไว้ในโค้ดด้วยชื่อไฟล์แบบเต็มๆ วิธีนี้จะช่วยให้รู้ว่าไฟล์ในโปรเจ็กต์ Android ตั้งอยู่ที่ใด<br />

ในขณะเดียวกันการใช้ข้อความภายในแอพนั้นจะมีอยู่ 2 แบบ คือ การใช้ข้อความเพื่อเป็น<br />

หมายเหตุอธิบายการทำงานของแอพ และการใช้ข้อความเพื่อแสดงบนจอภาพและการประมวลผลใน<br />

แอพ<br />

m การเขียนข้อความเหตุอธิบายการทำงานของแอพมีอยู่ 2 แบบ คือการเขียนหมายเหตุ<br />

แบบอธิบายการทำงานของฟังก์ชั่น และการเขียนหมายเหตุแบบกำกับคำสั่งแต่ละบรรทัด<br />

ซึ่งการเขียนแบบอธิบายการทำงานของฟังก์ชั่นจะอธิบายได้ชัดเจนกว่า และคำสั่งที่พิมพ์<br />

เป็นอักษรตัวหนานั้นจะแสดงถึงคำสั่งที่อธิบายถึงในหัวข้อนั้นๆ ในบางชุดคำสั่งจำเป็นต้อง<br />

มีการเขียนคำสั่งอื่นๆ เพิ่มเติมเพื่อทำงานร่วมกัน เวลาใช้งานจริงนั้นจะมีรายละเอียดการ<br />

ทำงานย่อยมากกว่านี้<br />

m ข้อความต่างๆ ที่ใช้ในแอพนั้น เราสามารถกำหนดไว้ในตัวแปรแบบโกลบอล (Global) ได้<br />

ไว้เราค่อยมาพูดถึงการใช้ตัวแปรประเภทนี้อีกครั้งในบทที่ 4 “ส่วนการติดต่อกับผู้ใช้งาน<br />

(User Interface)” โดยเราจะกำหนดค่าของข้อความต่างๆ ไว้ในตัวแปรแบบโกลบอลเพื่อ<br />

ให้สามารถอ้างอิงถึงข้อความดังกล่าวแล้วนำมาใช้ในส่วนของการติดต่อกับผู้ใช้งาน และ<br />

ในส่วนอื่นๆ ได้ ทำให้แอพมีลักษณะที่แยกส่วนของเลย์เอาต์และข้อมูลออกจากกันอย่าง<br />

ชัดเจน<br />

เดี๋ยวในบทที่ 2 เราจะมาพูดถึงการใช้โปรแกรม Eclipse และปลั๊กอิน Android SDK เพื่อ<br />

พัฒนาแอพ, ศึกษาโครงสร้างของโปรเจ็กต์, จัดการไฟล์ต่างๆ ภายในโปรเจ็กต์, การสร้างแพ็คเกจ,<br />

การลงทะเบียนและการเผยแพร่แอพด้วยโปรแกรม Eclipse กัน<br />

ในชุดพัฒนาแอพบนแอนดรอยด์มีเครื่องมือสร้างระบบจำลองการทำงานของระบบปฏิบัติการ<br />

แอนดรอยด์ติดมาด้วย ซึ่งเราจะใช้เพื่อทดสอบการทำงานของแอพเบื้องต้น ก่อนที่จะนำไปทดสอบการ<br />

ใช้งานบนอุปกรณ์แอนดรอยด์จริงๆ อีกครั้ง ซึ่งชุดคำสั่งที่ใช้ในหนังสือเล่มนี้ได้ถูกทดสอบบนระบบ<br />

ปฏิบัติการแอนดรอยด์เวอร์ชั่น 2.1<br />

ลักษณะของแอพที่ดี<br />

แอพที่ดีจะต้องมีลักษณะอยู่ 3 ประการ คือ มีแนวคิดที่ดี มีการออกแบบที่ดี และมีการเขียนชุด<br />

คำสั่งที่ดี ซึ่งในความเป็นจริงคุณจะพบว่าในบางแอพมีลักษณะการทำงานที่ดี แต่กลับมีหน้าตาไม่<br />

สวยงามเท่าไหร่ อาจเป็นผลจากการที่มีผู้พัฒนาแค่คนเดียวซึ่งมีความถนัดด้านการเขียนแอพเท่านั้น<br />

แต่ไม่ได้ถนัดในเรื่องของการออกแบบ ทาง Google คงรู้ถึงปัญหานี้ เลยจัดเตรียมข้อมูลที่ช่วยแนะนำ<br />

ด้านการออกแบบแอพ, ขั้นตอนการทำงาน, การออกแบบส่วนติดต่อกับผู้ใช้งาน และการวางโครงสร้าง<br />

ของเมนูที่จะใช้ในแอพเอาไว้ให้ คุณสามารถเข้าไปดูได้ที่ http://developer.android.com/guide/<br />

practices/ui_guidelines/


การพัฒนาแอนดรอยด์<br />

ความเข้ากันได้ของแอพและระบบปฏิบัติการรุ่นใหม่ๆ<br />

ค่าของ API Level จะเป็นตัวกำหนดความเข้ากันได้ของแอพและระบบปฏิบัติการแอนดรอยด์<br />

ค่านี้จะเป็นตัวกำหนดชนิดของฮาร์ดแวร์ที่จะใช้ในการติดตั้งระบบปฏิบัติการแอนดรอยด์เช่นกัน<br />

ถ้าซอฟต์แวร์มีการเปลี่ยนแปลง ก็จะส่งผลถึงการทำงานของฮาร์ดแวร์ด้วย ดังนั้นข้อควรคำนึงในการ<br />

พัฒนาแอพเพื่อให้รองรับการทำงานกับแอนดรอยด์เวอร์ชั่นอื่นๆ ในอนาคตจึงมีดังนี้<br />

m หลีกเลี่ยงการใช้ API ที่สนับสนุนจาก Google<br />

m หลีกเลี่ยงการแก้ไขค่าของระบบโดยตรง โดยไม่มีการยืนยันจากผู้ใช้งาน ซึ่งจากเหตุผลใน<br />

ด้านความปลอดภัย ในระบบปฏิบัติการแอนดรอยด์เวอร์ชั่นหลังๆ จะป้องกันการเข้าถึงใน<br />

ส่วนนี้หากไม่มีการยืนยันจากผู้ใช้งาน เช่น การยืนยันการใช้งานระบบ GPS หรือการใช้<br />

งานการเชื่อมโยงข้อมูลระหว่างเครือข่าย (Data Roaming)<br />

m หลีกเลี่ยงการออกแบบจอภาพที่ซับซ้อนมากเกินไป ทำให้หน่วยประมวลผลทำงานหนัก<br />

และอาจทำให้ระบบปฏิบัติการล้มเหลวได้<br />

m ตรวจสอบรายละเอียดของฮาร์ดแวร์เพื่อให้ทราบถึงข้อจำกัดต่างๆ เพื่อเลี่ยงข้อผิดพลาดที่<br />

คาดไม่ถึง<br />

m ถ้าแอพนั้นๆ รองรับการแสดงผลแบบแนวตั้งและแนวนอนได้ ก็ควรพิจารณาถึงขนาดของ<br />

จอภาพด้วยว่าทำงานได้สะดวกและแสดงผลได้ครบถ้วนหรือไม่<br />

Google ไม่มีการรับรองถึงความเข้ากันได้ระหว่างแอพและระบบปฏิบัติการรุ่นเก่า ดังนั้นจึงควร<br />

เลือกใช้เวอร์ชั่นของ SDK ให้เหมาะสมและระมัดระวังเวลาเลือกใช้คุณสมบัติใหม่ๆ ด้วยว่ามีความเข้า<br />

กันได้กับฮาร์ดแวร์ที่ใช้อยู่หรือไม่<br />

ประสิทธิภาพในการทำงานของแอพ<br />

เช่นเดียวกับหลักการพัฒนาแอพให้มีความเข้ากันได้กับระบบปฏิบัติการแอนดรอยด์เวอร์ชั่น<br />

ต่างๆ คุณจะต้องทำการออกแบบและทดสอบประสิทธิภาพการทำงานของแอพโดยใช้แนวคิดเหล่านี้<br />

m เลือกใช้ไลบรารีของแอนดรอยด์เป็นหลัก ยกเว้นว่าไม่มีคำสั่งที่ต้องการจึงค่อยเรียกใช้<br />

ไลบรารีของจาวาแทน เพราะไลบรารีของแอนดรอยด์นั้นถูกออกแบบให้เหมาะกับการ<br />

ทำงานบนอุปกรณ์แอนดรอยด์อยู่แล้ว ซึ่งจะมีขนาดที่เล็กและทำงานได้เร็วกว่า<br />

m ตรวจสอบการใช้หน่วยความจำของแอพ, ใช้งานตัวแปร, พยายามใช้ชุดคำสั่งที่มีอยู่แล้ว<br />

แทนการสร้างชุดคำสั่งขึ้นใหม่, หลีกเลี่ยงการสร้างออบเจ็กต์ต่างๆ โดยไม่จำเป็นเพื่อ<br />

บริหารการใช้หน่วยความจำให้มีประสิทธิภาพ ซึ่งคุณสามารถตรวจสอบการใช้หน่วย<br />

ความจำได้โดยใช้ Dalvik Debug Monitor Server (DDMS) สำหรับวิธีใช้เครื่องมือนี้<br />

ดูได้ในบทที่ 12 “การตรวจสอบการทำงานของแอพ”<br />

m ใช้เครื่องมือ LogCat เพื่อช่วยในการดีบั๊กแอพ และตรวจสอบข้อผิดพลาดต่างๆ<br />

m พยายามทดสอบการทำงานของแอพภายใต้สภาพแวดล้อมต่างๆ และบนฮาร์ดแวร์ต่างๆ<br />

ให้มากที่สุดเท่าที่จะเป็นไปได้<br />

13


14 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

เครื่องมือที่ใช้ในการพัฒนาแอพ<br />

ชุดเครื่องมือที่ใช้พัฒนาแอพบนแอนดรอยด์ประกอบไปด้วย แพลตฟอร์ม เครื่องมือ ตัวอย่าง<br />

ชุดคำสั่ง และเอกสารประกอบการพัฒนาแอพบนระบบปฏิบัติการแอนดรอยด์ ซึ่งทั้งหมดนี้จะทำงาน<br />

ร่วมกับชุดเครื่องมือในการพัฒนาแอพภาษาจาวา โดยจะมีลักษณะเป็นโปรแกรมปลั๊กอินเพื่อเชื่อมต่อ<br />

กับเครื่องมือเขียนแอพ อย่างเช่น Eclipse Integrated Development Environment<br />

การติดตั้งและอัพเกรด<br />

มีเว็บไซต์หลายแห่งบนอินเตอร์เน็ตที่พูดถึงขั้นตอนการติดตั้งและใช้งานชุดพัฒนาแอพบน<br />

แอนดรอยด์ อย่างเช่น http://developer.android.com/sdk/ เป็นเว็บหนึ่งที่มีข้อมูลที่เป็นประโยชน์<br />

อยู่มากมาย และมีข้อมูลเกี่ยวกับขั้นตอนการติดตั้งชุดพัฒนาแอพบนระบบปฏิบัติการแอนดรอยด์โดย<br />

ละเอียดด้วย ซึ่งคุณสามารถศึกษาและทดลองติดตั้งได้ด้วยตนเอง ตามขั้นตอนโดยสรุปดังนี้<br />

1. ติดตั้งชุดพัฒนาแอพภาษาจาวา หรือ Java Development Kit (ในกรณีที่จะพัฒนา<br />

แอพบนแอนดรอยด์เวอร์ชั่น 2.1 ขึ้นไป จะต้องติดตั้ง JDK เวอร์ชั่น 6 สำหรับใน<br />

แอนดรอยด์เวอร์ชั่นก่อนหน้านี้จะทำงานร่วมกับ JDK เวอร์ชั่น 5)<br />

2. ติดตั้งโปรแกรม Eclipse Classic (เช่น เวอร์ชั่น 3.5.2) ถ้าคุณจะติดตั้งบน Windows<br />

ให้คลายไฟล์นี้ก่อนใช้งาน<br />

3. ติดตั้งชุดพัฒนาแอพบนแอนดรอยด์ หรือ Android Starter Package SDK (เช่น<br />

เวอร์ชั่น r06) เช่นเดียวกัน ถ้าคุณจะติดตั้งบน Windows ให้คลายไฟล์นี้ก่อนใช้งาน<br />

4. เมื่อติดตั้งโปรแกรมทั้งหมดเสร็จเรียบร้อยแล้ว ก็เปิด Eclipse ขึ้นมา และไปที่เมนู Help<br />

→ Install New Software แล้วพิมพ์ข้อความ https://dl-ssl.google.com/android/<br />

eclipse/ ลงไปเพื่อติดตั้งชุดเครื่องมือพัฒนาแอพแอนดรอยด์<br />

5. เลือกเมนู Window → Preferences (ถ้าติดตั้งบน Mac ให้เลือก Eclipse →<br />

Preferences) เลือก Android ตามด้วยมองหาโฟลเดอร์ที่คุณคลายไฟล์ Android SDK<br />

ไว้ แล้วเลือก Apply<br />

6. เลือกเมนู Window → Android SDK and AVD Manager → Available<br />

Packages เลือกหัวข้อที่คุณต้องการจะติดตั้ง (เช่น เอกสารประกอบการเขียนแอพ<br />

สำหรับ Google API, Android API, SDK Platform เป็นต้น)<br />

7. จากเมนู Android SDK and AVD Manager เราสามารถเลือกให้สร้างระบบปฏิบัติการ<br />

แอนดรอยด์จำลองเพื่อทดสอบการทำงานของแอพ หรือเลือกให้ทดสอบแอพบนอุปกรณ์<br />

แอนดรอยด์ที่เชื่อมต่ออยู่กับเครื่องคอมพิวเตอร์โดยตรงก็ได้<br />

8. จากโปรแกรม Eclipse ให้เลือกเมนู Run → Run Configuration เพื่อสร้างค่าเริ่มต้น<br />

ระบบ จะได้เอาไว้ใช้กับแอพแอนดรอยด์ รวมทั้งกำหนดค่าของการดีบั๊กแอพด้วย ซึ่งเรา<br />

จะใช้ Android JUnit ในการตรวจหาข้อผิดพลาด


เครื่องมือที่ใช้ในการพัฒนาแอพ<br />

เมื่อเสร็จขั้นตอนเหล่านี้แล้ว เราก็จะได้สภาพแวดล้อมที่พร้อมสำหรับการพัฒนาแอพบน<br />

แอนดรอยด์ รวมถึงระบบจำลองที่ใช้ในการทดสอบแอพที่เราพัฒนาขึ้นแล้ว คุณสามารถตรวจสอบการ<br />

อัพเดตต่างๆ ที่เกี่ยวข้องกับชุดพัฒนาแอพที่คุณใช้อยู่ได้โดยเลือกเมนู Help → Software<br />

Updates<br />

ฟีเจอร์ต่างๆ ของแอนดรอยด์และระดับของ API<br />

ในแอนดรอยด์แต่ละเวอร์ชั่นจะมีการเพิ่มฟีเจอร์ใหม่ๆ เข้ามาหลายอย่างเพื่อเพิ่มประสิทธิภาพ<br />

ในการทำงาน รวมทั้งแก้ไขข้อผิดพลาดต่างๆ ที่พบในเวอร์ชั่นก่อนๆ และเพิ่มการรองรับการทำงานร่วม<br />

กับฮาร์ดแวร์รุ่นใหม่ๆ โดยทั่วไปแล้วเมื่อมีอุปกรณ์แอนดรอยด์รุ่นใหม่ออกสู่ตลาด อุปกรณ์นั้นจะติดตั้ง<br />

ระบบปฏิบัติการแอนดรอยด์เวอร์ชั่นล่าสุด ณ เวลานั้นมาด้วยเสมอ<br />

จากการที่ในเวอร์ชั่นหลังๆ มีการเพิ่มฟีเจอร์ใหม่เข้ามาหลายอย่าง เลยอาจจะทำให้อุปกรณ์<br />

แอนดรอยด์ที่ใช้เวอร์ชั่นก่อนๆ ไม่สามารถทำงานร่วมกับเวอร์ชั่นใหม่ๆ ได้ จึงมีการตั้งข้อกำหนดขึ้นมา<br />

เพื่อใช้ตรวจสอบการเข้ากันได้ระหว่างระบบปฏิบัติการและแอพแอนดรอยด์ ซึ่งก็คือค่าของระดับ API<br />

หรือที่เรียกว่า API Level นั่นเอง<br />

ตัวอย่างด้านล่างนี้เป็นการสรุปย่อเกี่ยวกับระบบปฏิบัติการต่างๆ และฟีเจอร์หลักๆ ซึ่งผู้พัฒนา<br />

ควรใช้ข้อมูลนี้ในการพิจารณา API Level<br />

Cupcake: Android OS 1.5, API Level 3 (เปิดตัวเมื่อ 30 เม.ย. พ.ศ. 2552)<br />

m ใช้ Linux Kernel 2.6.27<br />

m มีแป้นพิมพ์แบบซอฟต์แวร์ และรองรับแป้นพิมพ์เพิ่มเติมจากผู้พัฒนาอื่นๆ ได้<br />

m รองรับ App Widget Framework<br />

m Live Folders<br />

m รองรับการเล่นและบันทึกไฟล์ข้อมูลเสียง<br />

m มีชุดคำสั่งที่ใช้ในการเล่นไฟล์ MIDI<br />

m มี API ที่ใช้ในการบันทึกข้อมูลวิดีโอ<br />

m รองรับหูฟังบลูทูธแบบสเตอริโอ<br />

m ยกเลิกสิทธิการเข้าใช้งานระดับ Root<br />

m รองรับการวิเคราะห์เสียง (Speech Recognition) โดยใช้งานผ่านคลาวด์เซอร์วิสชื่อ<br />

RecognizerIntent<br />

m ปรับปรุงการใช้งาน GPS ให้มีความรวดเร็วยิ่งขึ้นด้วยเทคโนโลยี AGPS<br />

Donut: Android OS 1.6, API Level 4 (เปิดตัวเมื่อ 15 ก.ย. พ.ศ. 2552)<br />

m ใช้ Linux Kernel 2.6.29<br />

m รองรับการแสดงผลบนจอภาพหลายขนาด<br />

m มี API Gesture<br />

m มีโมดูลแปลงข้อมูลข้อความเป็นข้อมูลเสียง (Test-to-speech)<br />

m มีการติดตั้งระบบค้นหาแบบเร็วโดยใช้ SearchManager<br />

m รองรับการทำงานบนเครือข่ายส่วนตัวแบบเสมือน (Virtual Private Networking)<br />

15


16 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

Eclair: Android OS 2.0, API Level 5 (เปิดตัวเมื่อ 26 ต.ค. พ.ศ. 2552)<br />

Android OS 2.0.1, API Level 6 (เปิดตัวเมื่อ 3 ธ.ค. พ.ศ. 2552)<br />

Android OS 2.1, API Level 7 (เปิดตัวเมื่อ 12 ม.ค. พ.ศ. 2553)<br />

m มี API Synchronize<br />

m เรียกใช้งาน Quick Contact ได้จากทุกแอพ<br />

m รองรับ HTML5<br />

m รองรับการทำงานร่วมกับ Microsoft Exchange<br />

m คลาส MotionEvent รองรับการสัมผัสจอภาพแบบหลายจุด<br />

m รองรับภาพวอลเปเปอร์แบบเคลื่อนไหว<br />

FroYo: Android OS 2.2, API Level 8 (เปิดตัวเมื่อ 20 พ.ค. พ.ศ. 2553)<br />

m ใช้ Linux Kernel 2.6.32<br />

m รองรับการประมวลผลแบบ Just-in-Time (JIT) เพื่อประสิทธิภาพการทำงานที่รวดเร็วขึ้น<br />

m ใช้บลูทูธสั่งให้หมุนเบอร์โทรศัพท์ด้วยเสียงได้<br />

m การสัมผัสจอภาพแบบหลายจุดสามารถทำได้ละเอียดมากขึ้น<br />

m รองรับการทำงานแบบคลาวด์ (Cloud to Device API)<br />

m สามารถติดตั้งแอพลงบนหน่วยความจำภายนอกได้ เช่น SD Memory Card<br />

m รองรับการทำงานแบบ Wi-Fi Hotspot<br />

m แสดงภาพและวิดีโอแบบทัมบ์เนลได้<br />

m แป้นพิมพ์รองรับการป้อนข้อมูลหลายภาษาได้<br />

m เมื่อแอพมีข้อผิดพลาด จะมีการแจ้งข้อมูลกลับไปยังแอพ Market<br />

ในแอนดรอยด์เวอร์ชั่นหลังๆ ทางผู้พัฒนาพยายามเว้นระยะการปล่อยเวอร์ชั่นใหม่ๆ ออกมาเพื่อ<br />

ลดการเปลี่ยนแปลงค่า API Level และช่วยให้ทางผู้ผลิตฮาร์ดแวร์สามารถควบคุมรายละเอียดของ<br />

ฮาร์ดแวร์ให้รองรับ API Level ได้ง่ายขึ้น ทำให้ลดความถี่ในการอัพเดตระบบปฏิบัติการให้น้อยลงและ<br />

เพิ่มความเสถียรของระบบได้ ดังนั้นในการออก API Level ใหม่ๆ จะช่วยเพิ่มคุณสมบัติต่างๆ และ<br />

ประสิทธิภาพได้ชัดเจนมากยิ่งขึ้น<br />

ระบบจำลองการทำงานและการดีบั๊กแอพแอนดรอยด์<br />

ระบบจำลองการทำงาน หรือที่เรียกว่า Emulator นั้นมีลักษณะจำลองการทำงานของระบบ<br />

ปฏิบัติการแอนดรอยด์ให้มาแสดงผลบนเครื่องคอมพิวเตอร์ ทำให้สะดวกแก่การทดสอบและแก้ไขข้อ<br />

ผิดพลาดของแอพที่พัฒนาขึ้น โดยสามารถเลียนแบบการทำงานของชุดคำสั่ง ARM ได้ เหมือนกับการ<br />

ทำงานบนอุปกรณ์แอนดรอยด์จริงๆ แต่ก็ยังมีการทำงานบางอย่างที่ไม่อาจทดสอบบนระบบจำลองได้<br />

เช่น การทดสอบการโทรเข้าของอุปกรณ์แอนดรอยด์, การปรับเปลี่ยนมุมมองของจอภาพด้วยการ<br />

ตะแคง หรือการใช้งานตัวตรวจจับต่างๆ ระบบจำลองการทำงานจะถูกใช้ในการทดสอบการทำงานและ<br />

แก้ไขข้ผิดพลาดเบื้องต้นของแอพที่พัฒนาขึ้นก่อนที่จะนำไปทดสอบการใช้งานบนอุปกรณ์แอนดรอยด์<br />

จริงๆ


เครื่องมือที่ใช้ในการพัฒนาแอพ<br />

ก่อนการใช้งานระบบจำลองนั้น คุณจะต้องกำหนดคุณสมบัติของระบบที่จะจำลองการทำงาน<br />

ก่อน ซึ่งโปรแกรม Eclipse ได้ใช้เครื่องมือชื่อ Android Virtual Devices (AVD) ในการจัดการระบบ<br />

จำลอง ซึ่งในระบบจำลองที่ใช้งานนั้น มีคีย์ลัดให้ใช้งานด้วย ดูได้ตามที่แสดงในตารางที่ 1.4<br />

ตารางที่ 1.4 ชุดควบคุมระบบจำลองการทำงานของแอนดรอยด์<br />

คีย์ลัด การทำงานที่ถูกจำลองขึ้นมา<br />

คีย์ Escape ปุ่ม Back<br />

คีย์ Home ปุ่ม Home<br />

คีย์ F2, PageUp ปุ่ม Menu<br />

คีย์ Shift+F2, Page Down ปุ่ม Start<br />

คีย์ F3 ปุ่ม Call/Dial<br />

คีย์ F4 ปุ่ม Hangup/EndCall<br />

คีย์ F5 ปุ่ม Search<br />

คีย์ F7 ปุ่ม Power<br />

คีย์ Ctrl+F3, Ctrl+KEYPAD_5 ปุ่ม Camera<br />

คีย์ Ctrl+F5, KEYPAD_PLUS ปุ่ม Volume Up<br />

คีย์ Ctrl+F6, KEYPAD_MINUS ปุ่ม Volume Down<br />

คีย์ KEYPAD_5<br />

DPAD กลาง<br />

คีย์ KEYPAD_4, KEYPAD_6<br />

DPAD ซ้าย, DPAD ขวา<br />

คีย์ KEYPAD_8, KEYPAD_2 DPAD ขึ้น, DPAD ลง<br />

คีย์ F8<br />

เปิดปิดเครือข่ายโทรศัพท์<br />

คีย์ F9 เปิดปิดการทำางานของโปรไฟล์ลิ่ง<br />

คีย์ Alt+ENTER<br />

เปิดปิดโหมด Fullscreen<br />

คีย์ Ctrl+T<br />

เปิดปิดโหมด Trackball<br />

คีย์ Ctrl+F11, KEYPAD_7 หมุนเปลี่ยนทิศทางของจอภาพให้เป็นแบบอันที่แล้วหรือแบบถัดไป<br />

คีย์ Ctrl+F12, KEYPAD_9<br />

17<br />

โดยปกติแล้วการทดสอบการทำงานของแอพบนโทรศัพท์แอนดรอยด์จริงๆ เป็นการทดสอบที่ดี<br />

ที่สุด เพราะเป็นการทดสอบการทำงานจริงๆ บนฮาร์ดแวร์ ในขณะที่การทำงานบางอย่างจะไม่สามารถ<br />

ทำได้บนระบบจำลอง เวลาทดสอบบนเครื่องแอนดรอยด์จริงๆ นั้น เราต้องใช้วิธีการดีบั๊กผ่านทาง<br />

พอร์ต USB ที่โทรศัพท์เชื่อมต่ออยู่ ซึ่งการเชื่อมต่อนี้จะต้องใช้ไดรเวอร์ USB (ไดรเวอร์ที่ว่านี้มีรวมไว้<br />

อยู่แล้วในชุดพัฒนาแอพแอนดรอยด์)<br />

การเชื่อมต่อนี้จะต้องมีการกำหนดค่าระบบเพื่อเปิดการใช้งานในแบบผู้พัฒนาแอพ ซึ่งสามารถ<br />

กำหนดได้โดยไปที่ Home Screen แล้วเลือก MENU → Settings → Applications →<br />

Unknown sources และเลือก MENU → Settings → Applications → Development →<br />

USB debugging เพื่อเปิดช่องทางการติดตั้งแอพลลิเคชั่นผ่านทางพอร์ต USB ซึ่งรายละเอียดเหล่า<br />

นี้จะพูดถึงในบทที่ 12


18 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

การดีบั๊กแอพแอนดรอยด์ด้วยบริดจ์<br />

ในบางครั้งเราอาจจะต้องใช้คำสั่งคอมมานด์ไลน์ (Command Line) เพื่อควบคุมการทำงานของ<br />

อุปกรณ์แอนดรอยด์ผ่านทางพอร์ต USB โดยเราสามารถดีบั๊กแอพด้วย Android Debug Bridge<br />

ซึ่งมีรวมมากับชุดพัฒนาแอพแอนดรอยด์อยู่แล้ว ตัวอย่างการเชื่อมต่อกับอุปกรณ์แอนดรอยด์ในกรณี<br />

ที่คอมพิวเตอร์ของคุณใช้ระบบปฏิบัติการลีนุกซ์ ให้พิมพ์คำสั่งดังนี้<br />

> adb shell<br />

จะเห็นได้ว่าคำสั่งของลีนุกซ์มีลักษณะคล้ายกับคำสั่งที่ใช้ในระบบปฏิบัติการยูนิกซ์ (UNIX)<br />

เราสามารถใช้คำสั่ง exit เพื่อออกจากเชลล์ได้ และสามารถเพิ่มเติมคำสั่งอื่นๆ รวมลงไปใน<br />

คอมมานด์เดียวกันนี้เพื่อสั่งให้ทำงานโดยไม่จำเป็นต้องเข้าและออกจากเชลล์ ดังตัวอย่างนี้<br />

> adb shell mkdir /sdcard/app_bkup/<br />

คำสั่ง pull จะใช้ในการก็อบปี้ไฟล์ออกมาจากอุปกรณ์แอนดรอยด์ และเปลี่ยนชื่อไฟล์นั้น<br />

ดังตัวอย่างนี้<br />

> adb pull /system/app/VoiceSearchWithKeyboard.apk VSwithKeyboard.apk<br />

คำสั่ง push ใช้ในการก็อบปี้ไฟล์ไปยังอุปกรณ์แอนดรอยด์<br />

> adb push VSwithKeyboard.apk /sdcard/app_bkup/<br />

ถ้าต้องการลบแอพ อย่างเช่น com.dummy.game ออกจากอุปกรณ์แอนดรอยด์ ให้พิมพ์ดังนี้<br />

> adb uninstall com.dummy.game<br />

คำสั่งเหล่านี้เป็นคำสั่งที่ใช้งานบ่อยๆ แต่ความจริงยังมีมากกว่านี้อีก ซึ่งคุณสามารถหาดูได้ในในบทที่12<br />

การลงทะเบียนและเผยแพร่แอพ<br />

ถ้าคุณต้องการเผยแพร่แอพที่พัฒนาขึ้นเองผ่านทาง Android Market ละก็ คุณจะต้องลง<br />

ทะเบียนแอพก่อน ในการลงทะเบียนนั้น คุณต้องสร้างกุญแจส่วนตัวขึ้นมาก่อน (Private Key) และ<br />

เก็บกุญแจนี้ไว้ในที่ปลอดภัย ทีนี้เมื่อคุณพัฒนาแอพเสร็จและพร้อมที่จะเผยแพร่แล้ว ก็ให้ลงทะเบียน<br />

ด้วยกุญแจส่วนตัวที่สร้างไว้ก่อนหน้านี้ ในกรณีที่แอพของคุณมีการอัพเกรด คุณจะต้องลงทะเบียนการ<br />

อัพเดตด้วยกุญแจส่วนตัวนี้ด้วยเช่นกัน<br />

โปรแกรม Eclipse จะช่วยดำเนินการขั้นตอนข้างต้นนี้ให้อัตโนมัติ โดยคลิกขวาตรงโปรเจ็กต์<br />

แอพแอนดรอยด์ที่คุณต้องการจะลงทะเบียน แล้วเลือกเมนู Export… > Export Android Application<br />

โปรแกรม Eclipse จะให้คุณกำหนดรหัสผ่านเพื่อใช้ในการสร้างกุญแจส่วนตัว ซึ่งจะถูกจัดเก็บไว้<br />

เพื่อใช้ลงทะเบียนแอพพลิเคชั่น รวมถึงใช้ในการอัพเกรดแอพในอนาคตด้วย โดยไฟล์กุญแจส่วนตัวที่<br />

ได้จะมีนามสกุล APK เพียงเท่านี้โปรเจ็กต์แอพแอนดรอยด์ที่คุณสร้างกุญแจส่วนตัวไว้นี้ก็พร้อม<br />

อัพโหลดเข้าสู่ Android Market เพื่อเผยแพร่แล้ว


Android Market<br />

Android Market<br />

หลังจากที่คุณออกแบบ พัฒนา ทดสอบ และสร้างกุญแจส่วนตัวในแอพแล้ว คุณก็สามารถนำ<br />

ไปเผยแพร่ใน Android Market ได้เลย ในการเข้าใช้งาน Android Market ของ Google นั้น<br />

คุณต้องสร้างบัญชีผู้ใช้งานขึ้นมาก่อน ซึ่งมีค่าธรรมเนียมในการใช้งาน 25 ดอลลาร์ การเผยแพร่แอพ<br />

ใน Android Market ถือเป็นเรื่องที่น่าตื่นเต้นเสมอสำหรับผู้พัฒนาทั้งหลาย เพราะหลังจากการ<br />

อัพโหลดแอพเข้าสู่ Market แล้วเพียง 1 ชั่วโมง ผู้ใช้แอนดรอยด์จากทั่วทุกมุมโลกก็จะสามารถเข้าไป<br />

ดูรายละเอียด, ดาวน์โหลด, ให้คะแนน รวมถึงแสดงความเห็นเกี่ยวกับแอพของคุณได้ทันที การเผย<br />

แพร่แอพใน Android Market มีสิ่งที่ควรคำนึงถึงดังต่อไปนี้<br />

ข้อตกลงและลิขสิทธิ์ของผู้ใช้งานโปรแกรม<br />

ข้อมูลใดๆ ก็ตามที่มีการเผยแพร่จะต้องอยู่ภายใต้ข้อตกลงและลิขสิทธิ์ของผู้ใช้แอพของ Berne<br />

Convention โดยคุณจะต้องระบุลิขสิทธิ์การใช้แอพด้วยวันที่ที่ทำการเผยแพร่แอพ เช่น @2010<br />

ซึ่งขั้นตอนการระบุลิขสิทธิ์ในการใช้แอพนั้นจะอยู่ในบทที่ 4<br />

ข้อตกลงและลิขสิทธิ์ของผู้ใช้แอพ (End User License Agreement) เป็นข้อตกลงที่ทำขึ้น<br />

ระหว่างผู้ใช้และผู้พัฒนาแอพ ข้อตกลงและลิขสิทธิ์ของผู้ใช้แอพส่วนใหญ่นั้นจะมีการระบุสิทธิ์เป็น<br />

Grant of License, Copyright และ No Warranties การระบุสิทธิ์เหล่านี้ถือเป็นสิ่งจำเป็นโดยเฉพาะ<br />

กับแอพที่พัฒนาขึ้นเพื่อจำหน่าย สำหรับขั้นตอนและชุดคำสั่งที่ใช้จัดการในเรื่องนี้ เราจะพูดถึงอีกทีใน<br />

บทที่ 9 “การทำงานร่วมกับข้อมูล”<br />

การทำให้แอพเป็นที่แพร่หลายมากขึ้น<br />

ผู้ใช้สามารถค้นหาแอพได้จากหลายช่องทาง วิธีที่ผู้พัฒนาจะทำให้แอพของตัวเองเป็นที่รู้จัก<br />

อย่างแพร่หลายก็มีหลายวิธีเช่นกัน<br />

วิธีแรก โดยปกติแล้วผู้ใช้สามารถค้นหาแอพใหม่ได้โดยเลือกดูในกลุ่มของ “Just in” ดังนั้น<br />

คุณจึงควรกำหนดประเภทและกลุ่มของแอพให้ชัดเจน และสื่อถึงเป้าหมายการใช้งานของแอพนั้นๆ<br />

เช่น ระบุว่าเป็น Game หรือ Communication การเขียนรายละเอียดของแอพควรมีความกระชับ<br />

เข้าใจง่าย ในกรณีของแอพประเภทเกมนั้นจะมีการระบุกลุ่มย่อยลงไปอีกจึงควรกำหนดให้ชัดเจน<br />

หรือในกรณีที่แอพนั้นเป็นแอพที่เน้นความสนุกสนาน ไม่มีเป้าหมายหรือคะแนนใดๆ คุณอาจกำหนด<br />

กลุ่มให้อยู่ในส่วนของ Entertainment ก็ได้ อย่างไรก็ดี เนื่องจากใน Android Market นั้นมีแอพอยู่<br />

เยอะมาก และมีทยอยมาเพิ่มตลอดเวลา ดังนั้นแอพที่อยู่ในกลุ่ม “Just in” จึงอาจแสดงให้เห็นแค่<br />

1-2 วันเท่านั้น<br />

วิธีที่ 2 ที่จะช่วยให้แอพของคุณสามารถเข้าถึงกลุ่มผู้ใช้ได้มากที่สุดนั่นคือ การกำหนดคำค้น<br />

(Keyword) ประจำแอพ โดยคุณควรกำหนดให้ตรงกับชื่อ, รายละเอียด และประเภทของแอพ ในกรณี<br />

ที่แอพของคุณรองรับภาษาอื่นๆ นอกเหนือจากภาษาอังกฤษ คุณก็สามารถกำหนดคำค้นให้เป็นหลายๆ<br />

ภาษาได้เพื่อให้การค้นหามีประสิทธิภาพยิ่งขึ้น<br />

วิธีที่ 3 คือการทำให้แอพของคุณอยู่ในกลุ่ม “Top” ซึ่งแอพที่จะอยู่ในกลุ่มนี้ได้ต้องมียอดการ<br />

ดาวน์โหลดที่สูง มีผู้ใช้แสดงความคิดเห็นเป็นจำนวนมาก และได้คะแนนเยอะ อันเป็นผลมาจาก<br />

ประสิทธิภาพการทำงานของแอพและการแก้ไขข้อผิดพลาดได้อย่างฉับไว ซึ่งสิ่งเหล่านี้ส่งผลต่อยอด<br />

การดาวน์โหลดเป็นอย่างมาก ดังนั้นก่อนเปิดตัวแอพควรตรวจสอบการทำงานของแอพให้ดีก่อน เพราะ<br />

ถ้าคุณปล่อยแอพให้คนโหลดไปใช้กันทั้งๆ ที่ยังมีปัญหาอยู่ คงไม่มีอะไรแย่ไปกว่าการที่มีคนแสดงความ<br />

เห็นว่า “แอพนี้กินแบตมาก” หรือ “ผมลบแอพนี้ออกไปไม่ได้” อีกแล้ว<br />

19


20 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

Android Market ถือเป็นช่องทางหนึ่งที่ใช้ในการสื่อสารระหว่างผู้พัฒนาและผู้ใช้งาน ซึ่งผู้ใช้<br />

สามารถแจ้งข้อผิดพลาดต่างๆ รวมทั้งวิพากษ์วิจารณ์การทำงานของแอพผ่านทาง Android Market<br />

ได้ ซึ่งข้อมูลเหล่านี้ทางผู้พัฒนาก็จะรับไปแก้ไข ปรับปรุง และปล่อยตัวอัพเดตในเวอร์ชั่นต่อๆ ไป<br />

วิธีทำให้แอพมีความแตกต่างจากแอพอื่นๆ<br />

นอกเหนือจากการทำให้แอพเป็นที่รู้จักของคนทั่วไปแล้ว การทำให้แอพมีจุดเด่น ดูแตกต่างจาก<br />

แอพอื่นๆ ในกลุ่มเดียวกันก็ถือเป็นอีกสิ่งหนึ่งที่ทำให้การเผยแพร่แอพประสบความสำเร็จ การออกแบบ<br />

แอพที่ดี มีการทำงานที่รวดเร็ว และมีส่วนติดต่อกับผู้ใช้งานที่สวยงามน่าใช้ถือเป็นปัจจัยสำคัญที่ทำให้<br />

แอพเกิดความแตกต่าง แต่ควรระมัดระวังและหลีกเลี่ยงเรื่องการนำข้อมูลหรือสื่อต่างๆ ที่ติดลิขสิทธิ์<br />

มาใช้ในแอพด้วย<br />

การกำหนดราคาแอพ<br />

ก่อนที่จะส่งแอพสู่ Android Market คุณต้องกำหนดก่อนว่าแอพนั้นใช้ได้ฟรีหรือเป็นแอพที่ต้อง<br />

เสียเงิน ซึ่งการกำหนดที่ว่านี้มีอยู่หลายแบบด้วยกัน ไม่ว่าจะเป็น:<br />

m เป็นแอพที่ใช้ได้ฟรี ไม่มีค่าใช้จ่าย ผู้ใช้ทุกคนสามารถดาวน์โหลด ติดตั้ง และใช้งานแอพ<br />

ได้<br />

m เป็นแอพที่ใช้ได้ฟรี แต่จะมีข้อความโฆษณาติดมาด้วย ซึ่งทางผู้พัฒนาได้ทำข้อตกลงกับผู้<br />

สนับสนุนต่างๆ เอาไว้เพื่อรับค่าโฆษณาและเงินทุนในการพัฒนาแอพเหมือนที่เห็นในรูปที่<br />

1.1<br />

m เป็นแอพที่ต้องจ่ายเงินก่อนใช้ โดย Google จะหักค่าดำเนินการเก็บไว้เอง 30% ในบาง<br />

ประเทศที่ไม่มีการทำข้อตกลงเก็บค่าใช้จ่ายนี้กับทาง Google ผู้ใช้ในประเทศนั้นๆ ก็จะไม่<br />

สามารถดาวน์โหลดหรือใช้งานแอพนั้นๆ ได้ ในกรณีเช่นนี้ ผู้พัฒนาบางรายก็จะเลี่ยงไป<br />

จำหน่ายผ่านช่องทางอื่นแทน<br />

m เป็นแอพที่ใช้งานได้ฟรี แต่จะมีบางส่วนที่ใช้ไม่ได้จนกว่าจะจ่ายค่าแอพ วิธีนี้เหมือนกับการ<br />

เปิดโอกาสให้ผู้ใช้ได้ทดลองใช้งานดูก่อนเพื่อประกอบการตัดสินใจ ถ้าพึงพอใจค่อยซื้อ<br />

เวอร์ชั่นเต็มมาใช้งานก็ได้ อย่างเช่นว่าถ้าเป็นแอพเกมเวอร์ชั่นฟรี อาจเล่นได้แค่ 10 ด่าน<br />

แต่ถ้าเป็นเวอร์ชั่นเต็มจะเล่นได้ทุกด่าน เป็นต้น<br />

m ขายแต้ม หรือสิ่งของต่างๆ ในแอพ ซึ่งเกมบนเฟซบุ๊กใช้วิธีนี้กันเยอะ


Android Market<br />

21<br />

รูปที่ 1.1 ตัวอย่างแบนเนอร์โฆษณาจาก AdMob<br />

สังเกตดูจะเห็นว่าแอพที่ใช้ได้ฟรีจะมียอดการดาวน์โหลดที่สูง อย่างน้อยๆ ก็น่าจะอยู่ที่ 1,000<br />

คนในช่วงเดือนแรกที่มีการเปิดตัว แม้ว่าแอพนั้นจะมีข้อบกพร่องหรือด้อยประสิทธิภาพก็ตาม ผลลัพธ์<br />

ที่ได้นี้จะเป็นผลจากปัจจัยที่ได้กล่าวมาข้างต้น ซึ่งสามารถสร้างผลบวกหรือลบให้กับแอพของคุณได้<br />

การแสดงข้อความโฆษณาในแอพบนอุปกรณ์ประเภทพกพานั้นยังอยู่ในขั้นเริ่มต้น ยังไม่มี<br />

ลักษณะที่ดึงดูดใจให้ผู้ใช้อยากคลิกที่โฆษณาเหล่านั้นเท่าไหร่ ดังนั้นในปัจจุบันนี้ การทำรายได้โดยการ<br />

ขายแอพผ่านทางมาร์เก็ต (Market) จึงเป็นวิธีที่นิยมมากกว่า โดยหลังจากการเผยแพร่แอพไปแล้ว<br />

เราจะสามารถติดตามยอดการดาวน์โหลด และข้อคิดเห็นต่างๆ จากผู้ใช้งานเพื่อนำมาแก้ไขปรับปรุง<br />

แอพเพิ่มเติมได้ หรือถ้าแอพนั้นมีกระแสตอบรับที่ดีจากผู้ใช้ ก็อาจขึ้นราคาของแอพในภายหลังได้ด้วย<br />

การจัดการกับความคิดเห็นที่มีต่อแอพและการอัพเดต<br />

แอพที่ประสบความสำเร็จส่วนใหญ่ใน Android Market นั้น ทางผู้พัฒนาแอพจะติดตามข้อคิด<br />

เห็นและคำวิจารณ์ต่างๆ อย่างต่อเนื่อง แล้วนำมาปรับปรุงแก้ไขแอพให้ดียิ่งขึ้น และจัดหาตัวอัพเดตให้<br />

อย่างรวดเร็ว ทั้งหมดนี้ถือเป็นปัจจัยสำคัญที่จะทำให้แอพของคุณประสบความสำเร็จ


22 บทที่ 1 ก้าวแรกกับแอนดรอยด์<br />

จากสถิติของ Android Market แสดงให้เห็นว่าผู้ใช้ทุกๆ 200 คน จะมี 1 คนที่ให้คะแนนแอพที่<br />

ดาวน์โหลดมาใช้งาน ข้อมูลเหล่านี้มีความสำคัญต่อผู้พัฒนาแอพมาก แม้ว่าคำวิจารณ์นั้นจะเป็นไปใน<br />

ทางลบ เช่นว่า “โปรแกรมนี้ใช้บน HTC Hero ไม่ได้” แต่ก็นับว่ามีประโยชน์ที่ช่วยให้คุณนำมาปรับปรุง<br />

แอพให้ดีขึ้นได้<br />

เมื่อปรับปรุงแก้ไขแอพแล้ว ผู้ใช้จะรู้สึกดีว่าความคิดเห็นที่แสดงไปได้รับการตอบสนอง หลังจาก<br />

นั้นก็จะมีแนวโน้มสูงที่จะได้รับคำวิจารณ์ในทางที่ดีและมียอดการดาวน์โหลดที่สูงขึ้น ในส่วนของการ<br />

แจ้งการอัพเดตแอพนั้น คุณสามารถส่งข้อความเตือนให้แก่ผู้ใช้ได้เช่นกัน ซึ่งผู้ใช้จะอัพเดตหรือไม่<br />

อัพเดตก็ได้<br />

ทางเลือกอื่นๆ ที่นอกเหนือจาก Android Market<br />

นอกเหนือจาก Android Market แล้ว ยังมีช่องทางอื่นๆ ให้เลือกเผยแพร่แอพของคุณอีก<br />

มากมาย ซึ่งแต่ละแหล่งก็มีจุดเด่นแตกต่างกันไป เช่น ระบบการค้นหาแอพ, การเก็บค่าใช้จ่าย เป็นต้น<br />

โดยทางผู้ผลิตฮาร์ดแวร์แอนดรอยด์ยี่ห้อต่างๆ ก็มีการสร้างตลาดของตัวเองเช่นกันเพื่อสนับสนุนและ<br />

ให้บริการแก่ผู้ใช้ที่ใช้ฮาร์ดแวร์ของผู้ผลิตรายนั้นๆ ยกตัวอย่างเช่น ผู้ใช้โทรศัพท์แอนดรอยด์ของ<br />

Motorola ในประเทศจีนและแถบละตินอเมริกา จะสามารถเข้าใช้งานได้ที่ http://developer.<br />

motorola.com/shop4apps


23<br />

บทที่ 2<br />

การพัฒนาแอพเบื้องต้น:<br />

แอคทิวิตี้ และ อินเท็นต์<br />

แอพแอนดรอยด์แต่ละตัวนั้นสร้างมาจากแอนดรอยด์โปรเจ็กต์ โดยในบทนี้จะกล่าวถึง<br />

โครงสร้างของโปรเจ็กต์ และพื้นฐานเบื้องต้นที่ควรรู้ก่อนเริ่มพัฒนาแอพ จากนั้นเราก็จะพาคุณไป<br />

เรียนรู้ขั้นตอนการสร้างแอคทิวิตี้ (Activity) และอินเท็นต์ (Intent) กัน<br />

โครงสร้างการทำงานของแอพแอนดรอยด์<br />

การทำงานของแอพแอนดรอยด์จะประกอบขึ้นจากฟังก์ชั่นการทำงานต่างๆ ร่วมกัน ทำให้เมื่อ<br />

ติดตั้งลงบนโทรศัพท์แล้วจะรองรับการทำงานได้หลากหลาย เช่น เล่นไฟล์เพลง, ใช้เป็นนาฬิกาปลุก,<br />

จัดการสมุดโทรศัพท์, การแก้ไขข้อมูลในไฟล์ต่างๆ เป็นต้น ฟังก์ชั่นการทำงานของแอนดรอยด์จะถูก<br />

แบ่งออกเป็น 4 ประเภท ดังที่เห็นในตารางที่ 2.1<br />

ตารางที่ 2.1 ประเภทของคอมโพเน็นต์ที่ใช้ในระบบปฏิบัติการแอนดรอยด์<br />

ลักษณะการทางาน Java Base Class ตัวอย่างการทางาน<br />

การทำางานของผู้ใช้งาน Activity แก้ไขไฟล์ข้อมูล หรือเล่นเกม<br />

การประมวลผลเบื้องหลัง Service เล่นไฟล์เพลง หรือแสดงข้อมูลสภาพอากาศ<br />

การรับข้อความ BroadcastReceiver นาฬิกาปลุก หรือแจ้งเตือนเหตุการณ์ต่างๆ<br />

การเก็บและเรียกใช้ข้อมูล ContentProvider การเปิดสมุดโทรศัพท์<br />

แอพที่สร้างขึ้นจะต้องประกอบด้วยคอมโพเน็นต์ทั้ง 4 ส่วนนี้ เมื่อมีการเรียกใช้งานคอมโพเน็นต์<br />

ก็จะถูกสร้างขึ้นเป็นอินสแตนซ์ (Instance) ด้วยระบบปฏิบัติการแอนดรอยด์ โดยแอพอื่นๆ สามารถ<br />

เรียกใช้งานอินสแตนซ์เหล่านี้ได้เช่นกัน<br />

จากตารางข้างบนจะเห็นได้ว่าในระบบปฏิบัติการแอนดรอยด์นั้น มีคอมโพเน็นต์ที่ทำงานอยู่<br />

หลายประเภท คอมโพเน็นท์เหล่านี้มีวงจรการทำงานเช่นเดียวกับคอมโพเน็นท์ในระบบปฏิบัติการอื่นๆ<br />

คือมีสถานะการสร้าง, การใช้งาน, การเลิกใช้งาน และการทำลาย การทำงานเหล่านี้เราสามารถทำการ<br />

โอเวอร์ไรด์ (Override) เพื่อนำมาใช้งานในแอพของเราได้


24 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

ยกเว้น Java Base Class ชื่อ ContentProvider ที่คอมโพเน็นต์ของมันจะถูกเรียกใช้เมื่อมี<br />

ข้อความแบบอะซิงโครนัสที่เรียกว่า Intent เข้ามาเรียกใช้ ภายใน Intent จะประกอบด้วยข้อมูล<br />

ของแต่ละคอมโพเน็นต์ ซึ่งจะอธิบายถึงเมธอด (Method) ที่ใช้ในการส่งผ่านข้อมูลระหว่างคอมโพเน็นต์<br />

ส่วนในช่วงท้ายของบทนี้จะสาธิตให้เห็นการสร้างและเรียกใช้คอมโพเน็นต์ Activity ซึ่งจะใช้<br />

ในการติดต่อและโต้ตอบกับผู้ใช้งาน ส่วนการใช้งานคอมโพเน็นต์ Service และ BroadcastReceiver<br />

เดี๋ยวเราค่อยดูกันในบทที่ 3 “เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน”<br />

กรรมวิธี: การสร้างโปรเจ็กต์และแอคทิวิตี้<br />

มาเริ่มต้นสร้างโปรเจ็กต์แอนดรอยด์ด้วยโปรแกรม Eclipse Integrated Development (IDE)<br />

กัน ซึ่งมีขั้นตอนดังนี้<br />

1. ในโปรแกรม Eclipse เลือกเมนู File → New → Android Project หน้าต่างสำหรับ<br />

สร้างโปรเจ็กต์จะเปิดขึ้นมา<br />

2. พิมพ์ชื่อโปรเจ็กต์ลงไป ในที่นี้เราตั้งชื่อว่า SimpleActivityExample<br />

3. เลือก Build Target จากตัวเลือกที่มี โดยในส่วนนี้ควรเลือกให้ตรงกับเวอร์ชั่นของ<br />

Software Development Kit ที่คุณได้ติดตั้งไว้<br />

4. ตรง Application ให้ใส่ชื่อลงไป เช่น Example of Basic Activity<br />

5. ตรง Package ให้ใส่ชื่อลงไป เช่น com.cookbook.simple_activity<br />

6. ในการสร้างแอคทิวิตี้หลักนั้น ควรดูให้แน่ใจว่าได้เลือกตัวเลือก Create Activity และ<br />

กรอกในช่อง Activity (เช่น SimpleActivity) เรียบร้อยแล้ว<br />

แอคทิวิตี้ทั้งหมดถูกสร้างมาจากคลาส Activity หรือซับคลาส (Subclass) อื่นๆ ที่อยู่ภายใต้<br />

มัน โดยแอคทิวิตี้แต่ละอันจะเริ่มทำงานที่เมธอด onCreate() เสมอ ซึ่งเมธอดนี้เรามักจะใช้เพื่อ<br />

กำหนดค่าเริ่มต้นต่างๆ, การสร้างส่วนติดต่อกับผู้ใช้งาน หรือการสั่งให้เธรด (Thread) เริ่มทำงาน<br />

ถ้าในโปรเจ็กต์ยังไม่มีการสร้างแอคทิวิตี้หลัก คุณก็สามารถสร้างขึ้นได้ตามขั้นตอนดังนี้<br />

1. สร้างคลาสหลัก โดยในโปรแกรม Eclipse ให้คลิกขวาที่โปรเจ็กต์ เลือกเมนู New →<br />

Class และกำหนดค่าของซูเปอร์คลาส (Super Class) เป็น android.app.Activity<br />

2. สร้างเมธอด onCreate() โดยไปที่โปรแกรม Eclipse คลิกขวาที่คลาส แล้วเลือกเมนู<br />

Source → Override/Implement Methods


3. เช่นเดียวกับฟังก์ชั่นทั่วไปที่อยู่เหนือกว่า เราจะต้องเรียกใช้งานเมธอดของซูเปอร์คลาส<br />

ด้วย ซึ่งคำสั่ง super.onCreate() จะต้องถูกเรียกใช้งานเป็นคำสั่งแรกก่อนการใช้งาน<br />

แอคทิวิตี้ใดๆ ตามชุดคำสั่งที่ 2.1<br />

ชุดคำสั่งที่ 2.1 src/com/cookbook/simple_activity/SimpleActivity.java<br />

package com.cookbook.simple_activity;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

public class SimpleActivity extends Activity {<br />

โครงสร้างการทำางานของแอพแอนดรอยด์<br />

25<br />

}<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

}<br />

4. ถ้ามีการเรียกใช้ UI คุณก็สามารถกำหนดไว้ในไฟล์ XML ซึ่งอยู่ที่ไดเร็กทอรี res/layout/<br />

โดยในชุดคำสั่งที่ 2.2 คุณจะได้เห็นข้อมูลภายในไฟล์ main.xml<br />

5. คุณสามารถกำหนดเลย์เอาต์ของแอคทิวิตี้ได้โดยเรียกใช้ฟังก์ชั่น setContentView()<br />

และส่งค่า Resource ID ให้แก่เลย์เอาต์ ในที่นี้เรากำหนดเป็น R.layout.main ตามที่<br />

แสดงในชุดคำสั่งที่ 2.1<br />

ชุดคำสั่งที่ 2.2 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

6. กำหนดพร็อพเพอร์ตี้ (Property) ของแอคทิวิตี้ในไฟล์ AndroidMenifest.xml ตามชุด<br />

คำสั่งที่ 2.5


26 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

คุณสามารถกำหนดข้อความต่างๆ ที่จะใช้ในแอพลงไปในไฟล์ strings.xml ที่อยู่ในโฟลเดอร์<br />

res/values/ ตามที่แสดงในชุดคำสั่งที่ 2.3 ได้ ซึ่งแสดงให้เห็นถึงการกำหนดค่าของข้อความเป็นแบบ<br />

ค่าคงที่ที่สามารถนำไปใช้อ้างอิงเพื่อใช้งานในส่วนต่างๆ ของแอพ<br />

ชุดคำสั่งที่ 2.3 res/values/strings.xml<br />

<br />

<br />

Hello World, SimpleActivity!<br />

SimpleActivity<br />

<br />

ในหัวข้อถัดไปเราจะมาพิจารณาโครงสร้างของโปรเจ็กต์และไฟล์ที่มีการสร้างขึ้นในโปรเจ็กต์กัน<br />

โครงสร้างของไดเร็กทอรีและไฟล์ในโปรเจ็กต์<br />

รูปที่ 2.1 แสดงตัวอย่างของโปรเจ็กต์, โครงสร้างไดเร็กทอรี และไฟล์ต่างๆ ที่อยู่ภายใต้<br />

ไดเรกทอรีนั้นใน Elipse Package Explorer<br />

รูปที่ 2.1 โครงสร้างไดเร็กทอรีและไฟล์ต่างๆ ที่อยู่ภายใต้โปรเจ็กต์


ไฟล์ต่างๆ ที่ผู้พัฒนาจะต้องสร้างขึ้นเองมีดังนี้<br />

m โฟลเดอร์ src/ จะเก็บไฟล์จาวาแพ็คเกจที่ผู้พัฒนาสร้างขึ้น หรืออิมพอร์ตเข้ามาใช้งาน<br />

โดยในแต่ละแพ็คเกจสามารถมีไฟล์จาวาได้หลายๆ ไฟล์ แต่ละไฟล์จะแทนคลาสแต่ละ<br />

คลาส<br />

m โฟลเดอร์ res/layout/ จะเก็บไฟล์ XML ที่กำหนดค่าเลย์เอาต์ของแต่ละจอภาพ<br />

m โฟลเดอร์ res/values/ จะเก็บไฟล์ XML ที่กำหนดค่าอ้างอิงต่างๆ เพื่อใช้งานในไฟล์อื่นๆ<br />

m โฟลเดอร์ res/drawable-hdpi/ , res/drawable-mdpi และ res/drawable-ldpi จะ<br />

เก็บไฟล์รูปภาพที่ใช้ในแอพ ซึ่งจะแยกออกเป็นความละเอียดสูง กลาง และต่ำ<br />

m โฟลเดอร์ assets/ จะเก็บไฟล์ต่างๆ ที่ไม่ใช่มีเดียไฟล์ (Media File) ที่ใช้ในแอพพลิเคชั่น<br />

m AndroidManifest.xml เป็นไฟล์ที่เก็บข้อมูลของโปรเจ็กต์<br />

ส่วนไฟล์ที่ระบบสร้างขึ้นมาให้อัตโนมัติมีดังนี้<br />

m โฟลเดอร์ gen/ จะเก็บชุดคำสั่งที่ระบบสร้างขึ้นอัตโนมัติ รวมทั้งสร้างไฟล์ R.java ด้วย<br />

m ไฟล์ default.properties จะเก็บค่าต่างๆ ของโปรเจ็กต์<br />

ในส่วนของทรัพยากรหรือรีซอร์ส (Resource) ของแอพจะประกอบไปด้วยไฟล์ XML ซึ่งจะเก็บ<br />

ข้อมูลของเลย์เอาต์ โดยเป็นข้อมูลประเภทข้อความ, ลาเบลของอีลีเมนต์ UI และไฟล์ชนิดอื่นๆ เช่น<br />

ไฟล์ข้อมูลภาพหรือเสียง ในระหว่างที่ทำการคอมไพล์แอพนั้น ข้อมูลเหล่านี้จะถูกอ้างอิงและรวมเข้า<br />

อัตโนมัติร่วมกับคลาส R.java ซึ่งเครื่องมือชื่อ Android Asset Packaging Tool (aapt) จะสร้างไฟล์<br />

นี้ขึ้นมา โดยด้านล่างนี้คือชุดคำสั่งที่เกิดขึ้นหลังจากการสร้างโปรเจ็กต์<br />

ชุดคำสั่งที่ 2.4 gen/com/cookbook/simple_activity/R.java<br />

/* AUTO-GENERATED FILE. DO NOT MODIFY.<br />

*<br />

* This class was automatically generated by the<br />

* aapt tool from the resource data it found. It<br />

* should not be modified by hand.<br />

*/<br />

package com.cookbook.simple_activity;<br />

public final class R {<br />

public static final class attr {<br />

}<br />

public static final class drawable {<br />

public static final int icon=0x7f020000;<br />

}<br />

public static final class layout {<br />

public static final int main=0x7f030000;<br />

}<br />

public static final class string {<br />

public static final int app_name=0x7f040001;<br />

public static final int hello=0x7f040000;<br />

}<br />

}<br />

โครงสร้างการทำางานของแอพแอนดรอยด์<br />

27


28 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

จะเห็นว่ารีซอร์สแต่ละตัวได้ถูกกำหนดเข้ากับค่าตัวเลขคงที่ที่มีค่าไม่ซ้ำกัน ซึ่งเราจะใช้คลาสนี้ใน<br />

การอ้างอิงถึงรีซอร์สในชุดคำสั่งที่เราเขียนขึ้น ยกตัวอย่างเช่น การอ้างอิงถึงไฟล์เลย์เอาต์ชื่อ main.<br />

xml ในภาษาจาวา เราจะใช้ค่าของ R.layout.main ส่วนการอ้างอิงถึงค่าต่างๆ ในไฟล์ XML<br />

เดียวกันนั้น เราจะใช้คำสั่ง “@layout/main”<br />

การอ้างอิงถึงรีซอร์สจากภายในโปรแกรมจาวาหรือไฟล์ XML ใดๆ นั้น คุณสามารถดูตัวอย่างได้<br />

ในตารางที่ 2.2 การที่จะสร้าง button ID ขึ้นใหม่ ชื่อ home_button เราจะต้องใส่เครื่องหมายบวกลง<br />

ไปที่สตริง @+id/home_button ซึ่งรายละเอียดต่างๆ ที่เกี่ยวกับรีซอร์สนั้น ดูได้ในบทที่ 4 “ส่วนการ<br />

ติดต่อกับผู้ใช้งาน (User Interface)”<br />

ตารางที่ 2.2 ความแตกต่างระหว่างการอ้างอิงรีซอร์ทจากไฟล์จาวาและไฟล์ XML<br />

รีซอร์ส การอ้างอิงด้วยจาวา การอ้างอิงด้วย XML<br />

res/layouy/main.xml R.layout.main @layout/main<br />

res/drawable-hdpi/icon.png<br />

R.drawable.icon<br />

@drawable/icon<br />

@+id/home_button R.id.home_button @id/home_button<br />

R.string.hello @string/hello<br />

แอนดรอยด์แพ็คเกจและไฟล์กำหนดคุณลักษณะ (Manifest)<br />

ในบางครั้งเราจะเรียกโปรเจ็กต์แอนดรอยด์ว่าแอนดรอยด์แพ็คเกจ (Android Package) ซึ่งจะ<br />

แทนกลุ่มของคำสั่งจาวา แอนดรอยด์แพ็คเกจสามารถใช้ชื่อของจาวาแพ็คเกจเดียวกันได้ แต่ชื่อของ<br />

แอนดรอยด์แพ็คเกจห้ามตั้งซ้ำกัน<br />

เพื่อให้ระบบปฏิบัติการสามารถเข้าถึงค่าต่างๆ ของแอพได้ เราต้องประกาศคอมโพเน็นต์ที่จะใช้<br />

งานลงไปในไฟล์กำหนดคุณลักษณะ (Manifest) ซึ่งไฟล์ดังกล่าวจะเก็บข้อมูลของสิทธิ์ต่างๆ ที่แอพใช้<br />

ในการสั่งให้แอพทำงาน ในชุดคำสั่งที่ 2.5 จะแสดงถึงข้อมูลภายในไฟล์ AndroidManifest.xml<br />

ชุดคำสั่งที่ 2.5 AndroidManifest.xml<br />

<br />

<br />


โครงสร้างการทำางานของแอพแอนดรอยด์ 29<br />

android:label="@string/app_name"><br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

คำสั่งในบรรทัดแรกเป็นคำสั่งมาตรฐานที่ไฟล์ XML จะต้องมี เป็นการประกาศรูปแบบการเข้า<br />

รหัสภาษาของข้อมูลภายในไฟล์ XML อีลีเมนต์ชื่อ manifest จะประกาศชื่อของแอนดรอยด์แพ็คเกจ<br />

และเวอร์ชั่นของแพ็คเกจเอาไว้ ซึ่ง versionCode จะมีชนิดของข้อมูลเป็นตัวเลข ใช้ในการตรวจสอบ<br />

ว่าการติดตั้งแอพนั้นเป็นการติดตั้งเพื่ออัพเกรดหรือดาวน์เกรด ส่วน versionName จะเป็นค่าเวอร์ชั่น<br />

ที่ใช้แสดงในแอพ<br />

อีลีเมนต์ application จะประกาศถึงลาเบลและไอคอนที่ผู้ใช้เห็นจากในเมนูแอพของแอน<br />

ดรอยด์ โดยข้อมูลลาเบลจะเป็นข้อความสั้นๆ ที่แสดงผลอยู่ใต้ภาพไอคอน ข้อความนี้ควรมีความยาว<br />

ประมาณ 10 ตัวอักษร เพราะถ้ามีความยาวมากกว่านี้ การแสดงผลจะตัดข้อความส่วนที่เกิน 10 ตัว<br />

อักษรออกไป<br />

อีลีเมนต์ activity จะประกาศถึงแอคทิวิตี้หลักที่จะถูกเรียกใช้งานเมื่อแอพเริ่มทำงาน และชื่อ<br />

จะแสดงที่ไตเติ้ลบาร์เมื่อแอคทิวิตี้นั้นกำลังทำงาน ในส่วนนี้เราจะต้องกำหนดค่าของจาวาแพ็คเกจ<br />

ซึ่งในกรณีนี้เรากำหนดว่า com.cookbook.simple_activity.SimpleActivity เนื่องจากชื่อจาวา<br />

แพ็คเกจตรงกับชื่อของแอนดรอยด์แพ็คเกจ ในบางครั้งจึงอาจเขียนให้สั้นลงได้เป็น .SimpleActivity<br />

อีลีเมนต์ intent-filter จะประกาศในระบบปฏิบัติการแอนดรอยด์ให้รู้ว่ามีการเรียกใช้<br />

คอมโพเน็นต์ใดบ้าง ในส่วนนี้จะแตกต่างกันไปตามชนิดการทำงานของแต่ละแอพ<br />

อีลีเมนต์ uses-sdk จะประกาศระดับของ API Level ที่ต้องการในการรันแอพ อย่างโค้ด<br />

ด้านล่างนี้จะแสดงถึงการกำหนดค่า API Level<br />

<br />

เนื่องจากแอนดรอยด์ถูกออกแบบมาให้รองรับการทำงานในเวอร์ชั่นถัดไปด้วย ดังนั้นเราจึงต้อง<br />

กำหนดค่าของ maxSdkVersion เพื่อระบุค่า API Level สูงสุดที่แอพจะทำงานได้ เช่นเดียวกับค่าของ<br />

minSdkVersion ที่จะต้องระบุเพื่อกำหนดค่าต่ำสุดของ API Level ที่แอพจะทำงานได้


30 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

ในส่วนของ AndroidManifest จะเก็บข้อมูลของสิทธิ์พื้นฐานของระบบที่ต้องการเพื่อใช้ในการ<br />

รันแอพ ซึ่งสิทธิ์เหล่านี้จะกล่าวถึงในบทถัดๆ ไป<br />

การเปลี่ยนชื่อไฟล์ภายในโปรเจ็กต์<br />

บางครั้งคุณอาจต้องการเปลี่ยนชื่อของแอนดรอยด์โปรเจ็กต์ก็ได้ เพราะบางทีมีการเพิ่มไฟล์<br />

ต่างๆ ที่มีอยู่แล้วในโปรเจ็กต์อื่นไปรวมไว้ในโปรเจ็กต์อันใหม่ที่สร้างขึ้นมา หรือในระหว่างการพัฒนา<br />

แอพนั้นมีการเปลี่ยนชื่อของโปรเจ็กต์ใหม่ ซึ่งการเปลี่ยนค่าเหล่านี้จะส่งผลกระทบถึงโครงสร้างและ<br />

การอ้างอิงของไฟล์ต่างๆ ภายในโปรเจ็กต์ไดเร็กทอรี ซึ่งในชุดพัฒนาแอพบนแอนดรอยด์นั้นจะมีตัว<br />

ช่วยอยู่ อย่างในโปรแกรม Eclipse IDE ก็จะมีวิธีเปลี่ยนชื่อของโปรเจ็กต์หลายวิธี ดังนี้<br />

m การเปลี่ยนชื่อของแอนดรอยด์โปรเจ็กต์ มีขั้นตอนดังนี้<br />

1. คลิกขวาที่โปรเจ็กต์ และเลือก Refactor → Move เพื่อย้ายไปยังไดเร็กทอรี<br />

ใหม่<br />

2. คลิกขวาที่โปรเจ็กต์ และเลือก Refactor → Rename เพื่อเปลี่ยนชื่อของ<br />

โปรเจ็กต์<br />

m การเปลี่ยนชื่อของแอนดรอยด์แพ็คเกจ มีขั้นตอนดังนี้<br />

1. คลิกขวาที่แพ็คเกจ และเลือก Refactor → Rename เพื่อเปลี่ยนชื่อของ<br />

แพ็คเกจ<br />

2. ตรวจสอบข้อมูลในไฟล์ AndroidManifest.xml เพื่อให้แน่ใจว่าชื่อแพ็คเกจเปลี่ยน<br />

ไปแล้ว<br />

m การเปลี่ยนชื่อคลาส (ประเภท Activity, Service, BroadcastReceiver และ<br />

ContentProvider) มีขั้นตอนดังนี้<br />

1. คลิกขวาที่ไฟล์นามสกุล .java และเลือก Refactor → Rename เพื่อเปลี่ยน<br />

ชื่อของคลาส<br />

2. ตรวจสอบข้อมูลในไฟล์ AndroidManifest.xml เพื่อให้แน่ใจว่าชื่อคลาสเปลี่ยน<br />

ไปแล้ว<br />

ส่วนการแก้ไขชื่อของไฟล์อื่นๆ เช่น ไฟล์ XML คุณจะต้องแก้ไขชื่อในส่วนต่างๆ ที่มีการอ้างอิง<br />

ในคำสั่งจาวาด้วย<br />

วงจรการทำงานของแอคทิวิตี้<br />

แอคทิวิตี้แต่ละตัวในแอพนั้นจะมีวงจรชีวิตการทำงานของมันเอง เมื่อแอคทิวิตี้ถูกสร้างขึ้น<br />

ฟังก์ชั่น onCrate() จะเริ่มทำงาน แต่ถ้าแอคทิวิตี้นั้นทำงานอยู่แล้ว ฟังก์ชั่น onDestroy() จะเริ่ม<br />

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


วงจรการทำางานของแอคทิวิตี้<br />

31<br />

แอคทิวิตี้เริ่มทำงาน<br />

เมธอด onCreate() ทำงาน<br />

ผู้ใช้กลับไปยังแอคทิวิตี้<br />

เมธอด onStart() ทำงาน<br />

เมธอด onRestart() ทำงาน<br />

กระบวนการถูกยกเลิก<br />

เพื่อคืนหน่วยความจำ<br />

เมธอด onResume() ทำงาน<br />

แอคทิวิตี้<br />

กำลังทำงาน<br />

แอคทิวิตี้หลักทำงานต่อเมื่อ<br />

ไม่มีแอพอื่นกำลังทำงาน<br />

แอพอื่นต้องการหน่วย<br />

ความจำเพิ่มเติม<br />

มีแอคทิวิตี้อื่นกำลังทำงานอยู่<br />

เมธอด onPause() ทำงาน<br />

แอคทิวิตี้ไม่มีการเคลื่อนไหว<br />

แอคทิวิตี้หลักทำงานต่อเมื่อ<br />

ไม่มีแอพอื่นกำลังทำงาน<br />

เมธอด onStop() ทำงาน<br />

เมธอด onDestroy() ทำงาน<br />

ยกเลิกการทำงาน<br />

ของแอคทิวิตี้<br />

รูปที่ 2.2 วงจรการทำางานของแอคทิวิตี้ จาก http://developer.android.com<br />

กรรมวิธี: การใช้งานแอคทิวิตี้ต่างๆ<br />

ในส่วนนี้เราจะแสดงให้คุณเห็นวงจรการทำงานของแอคทิวิตี้โดยอ้างอิงการทำงานจากรูปข้าง<br />

บน ทุกๆ ครั้งที่มีการโอเวอร์ไรด์ฟังก์ชั่น คำสั่ง Toast ที่เพิ่มเข้ามาจะถูกเรียกใช้งานเพื่อแสดง<br />

ข้อความบนจอภาพ (รายละเอียดของวิดเจต Toast จะอยู่ในบทที่ 3)


32 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

ดูแอคทิวิตี้ได้ในชุดคำสั่งที่ 2.6 เมื่อคุณรันแอพแล้ว ให้ทดลองการทำงานหลายๆ กรณีดังนี้<br />

m ลองตะแคงจอภาพเพื่อสั่งให้ทำการทำลายและสร้างแอคทิวิตี้<br />

m กดปุ่ม Home เพื่อหยุดการทำงานของแอคทิวิตี้ชั่วคราว แต่ไม่ทำลายแอคทิวิตี้<br />

m กดที่ไอคอน Application เพื่อสร้างอินสแตนซ์ของแอคทิวิตี้ในขณะที่แอคทิวิตี้อันเก่ายัง<br />

ไม่ถูกทำลาย<br />

m ปล่อยให้จอภาพดับเพื่อหยุดการทำงานของแอคทิวิตี้และทำการเปิดจอภาพเพื่อสั่งให้<br />

แอคทิวิตี้ทำงานต่อ<br />

ชุดคำสั่งที่ 2.6 src/com/cookbook/activity_lifecycle/ActivityLifecycle.java<br />

package com.cookbook.activity_lifecycle;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.widget.Toast;<br />

public class ActivityLifecycle extends Activity {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Toast.makeText(this, "onCreate", Toast.LENGTH_SHORT).show();<br />

}<br />

@Override<br />

protected void onStart() {<br />

super.onStart();<br />

Toast.makeText(this, "onStart", Toast.LENGTH_SHORT).show();<br />

}<br />

@Override<br />

protected void onResume() {<br />

super.onResume();<br />

Toast.makeText(this, "onResume", Toast.LENGTH_SHORT).show();<br />

}<br />

@Override<br />

protected void onRestart() {<br />

super.onRestart();<br />

Toast.makeText(this, "onRestart", Toast.LENGTH_SHORT).show();<br />

}


วงจรการทำางานของแอคทิวิตี้<br />

@Override<br />

protected void onPause() {<br />

Toast.makeText(this, "onPause", Toast.LENGTH_SHORT).show();<br />

super.onPause();<br />

}<br />

33<br />

@Override<br />

protected void onStop() {<br />

Toast.makeText(this, "onStop", Toast.LENGTH_SHORT).show();<br />

super.onStop();<br />

}<br />

}<br />

@Override<br />

protected void onDestroy() {<br />

Toast.makeText(this, "onDestroy", Toast.LENGTH_SHORT).show();<br />

super.onDestroy();<br />

}<br />

จะเห็นได้ว่าการทำงานต่างๆ ที่ได้รับการสั่งการจากผู้ใช้นั้นจะส่งผลให้สถานะของแอคทิวิตี้<br />

เปลี่ยนไป เช่น การสร้าง หยุดพัก หรือทำลาย เป็นต้น ต่อไปเราจะมาทำความเข้าใจเพิ่มเติมถึงวิธีการ<br />

ควบคุมสถานะต่างๆ ของแอคทิวิตี้กัน<br />

กรรมวิธี: การทำงานแบบซิงเกิลทาสก์ (Single Task)<br />

การทำงานของแอพโดยทั่วไปจะป้องกันการเรียกใช้งานแอพซ้ำในขณะที่แอพนั้นทำงานอยู่แล้ว<br />

ซึ่งการป้องกันเช่นนี้จะช่วยหลีกเลี่ยงการสร้างอินสแตนซ์ของ Activity ขึ้นมาซ้ำกันเพื่อลดปริมาณการ<br />

ใช้หน่วยความจำที่ซ้ำซ้อน เราสามารถควบคุมลักษณะการทำงานของแอพเหล่านี้ได้ด้วยการกำหนดค่า<br />

ของ AndroidManifest<br />

เพื่อให้แน่ใจว่าอินสแตนซ์ที่สร้างขึ้นนั้นเป็นอินสแตนซ์ที่ทำงานเพียงอินสแตนซ์เดียว ไม่สามารถ<br />

เรียกซ้ำๆ ได้ เราจะต้องกำหนดค่าดังนี้<br />

android:launchMode=”singleInstance”<br />

โค้ดข้างบนนี้จะสั่งให้อินสแตนซ์ทำงานในลักษณะอินสแตนซ์หรือซิงเกิลอินสแตนซ์ และเพื่อให้<br />

แน่ใจว่างานหรือทาสก์ที่ถูกสร้างขึ้นนั้นทำงานแบบซิงเกิลทาสก์ ก็ให้กำหนดค่าดังนี้<br />

android:launchMode=”singleTask”<br />

โค้ดนี้เป็นการอนุญาตให้แอคทิวิตี้แชร์ข้อมูลกันได้อย่างง่ายดายในฐานะที่เป็นงานเดียวกัน<br />

นอกจากนี้ถ้าคุณปิดและเปิดแอพขึ้นใหม่ สถานะต่างๆ ของแอคทิวิตี้จะถูกเริ่มต้นใหม่ทั้งหมด<br />

ถ้าคุณต้องการให้แอพมีการจดจำสถานะล่าสุดของแอคทิวิตี้ในกรณีที่มีการเปิดปิดแอพละก็ ให้กำหนด<br />

ค่าดังนี้<br />

android:alwaysRetainTaskState=”true”


34 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

กรรมวิธี: การหมุนจอภาพ<br />

ในอุปกรณ์แอนดรอยด์ที่มีการติดตั้งตัวตรวจจับอัตราเร่ง (Accelerometer) จะตรวจสอบได้ว่า<br />

จอภาพกำลังอยู่ในแนวตั้งหรือแนวนอน ในการตะแคงจอภาพนั้น จะมีบางแอคทิวิตี้ที่ถูกทำลายและ<br />

สร้างขึ้นใหม่เพื่อรองรับการทำงานร่วมกับสถานะจอภาพใหม่ ในกรณีนี้สถานะเดิมของแอคทิวิตี้จะหาย<br />

ไป สิ่งเหล่านี้เป็นเรื่องหนึ่งที่ควรคำนึงถึงเวลาพัฒนาแอพ<br />

ดังนั้นในการหมุนจอภาพจึงจำเป็นจะต้องมีการจัดเก็บสถานะของแอคทิวิตี้ไว้ก่อนที่จะหมุน<br />

จอภาพ และหลังจากหมุนจอภาพเสร็จแล้วก็จะเรียกค่าสถานะดังกล่าวกลับคืนมา คำสั่งที่ใช้ในการ<br />

กำหนดสถานะการหมุนจอภาพคือ screenOrientation ตัวอย่างเช่น ถ้าเราต้องการกำหนดให้<br />

แอคทิวิตี้ทำงานในสถานะจอภาพแนวตั้ง ก็ให้เพิ่มคำสั่งในอีลีเมนต์ activity ดังนี้<br />

android:screenOrientation=”portrait”<br />

ขณะเดียวกันถ้าเราต้องการกำหนดให้แอคทิวิตี้ทำงานในสถานะจอภาพแนวนอน ก็ให้เพิ่มคำสั่ง<br />

ในอีลีเมนต์ activity ดังนี้<br />

android:screenOrientation=”landscape”<br />

อย่างไรก็ตามการทำงานในลักษณะนี้จะส่งผลให้เกิดการทำลายและเริ่มแอคทิวิตี้ขึ้นใหม่เมื่อมี<br />

การใช้งานแป้นพิมพ์แบบซอฟต์แวร์ ดังนั้นจึงต้องมีคำสั่งเพิ่มเติมเพื่อให้แอพรองรับการทำงานบน<br />

จอภาพทั้ง 2 แบบ และทำงานร่วมกับแป้นพิมพ์แบบซอฟต์แวร์ได้ด้วยการใช้คำสั่งดังนี้<br />

android:configChanges=”orientation|keyboardHidden”<br />

คำสั่งนี้สามารถใช้งานแบบเดี่ยวๆ หรือใช้งานร่วมกับคำสั่ง screenOrientaion ได้เพื่อกำหนด<br />

คุณสมบัติการทำงานของแอพ<br />

กรรมวิธี: การจัดเก็บและเรียกคืนข้อมูลภายในแอคทิวิตี้<br />

ให้ใช้คำสั่ง onSaveInstanceState() เพื่อจัดเก็บสถานะของแอคทิวิตี้ก่อนที่จะถูกทำลาย<br />

คำสั่งดังกล่าวจะเก็บสถานะไว้ และเมื่อมีการเรียกใช้คำสั่ง onRestoreInstanceState() ก็จะคืนค่า<br />

ดังกล่าวกลับมายังอินสแตนซ์แอคทิวิตี้ด้วยคำสั่งทั้ง 2 นี้ คุณสามารถควบคุมและจัดเก็บสถานะต่างๆ<br />

ของแอคทิวิตี้ในวงจรการทำงานได้<br />

คำสั่งนี้จะแตกต่างจากคำสั่ง onPause() ตรงที่ถ้าระบบมีการหยุดทำงานชั่วคราว ในกรณีที่<br />

ทรัพยากรไม่เพียงพอ คำสั่ง onPause() จะเรียกใช้คำสั่ง onSaveInstanceState() ก่อน แล้วจึง<br />

ทำลายแอคทิวิตี้นั้น


ชุดคำสั่งที่ 2.7 จะแสดงถึงการจัดก็บและเรียกคืนค่าสถานะของอินสแตนซ์ โดยใช้ตัวแปรแบบ<br />

ข้อความและตัวแปรแบบทศนิยมในการเก็บค่าดังกล่าว<br />

float[] localFloatArray = {3.14f, 2.718f, 0.577f};<br />

String localUserName = "Euler";<br />

มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

ชุดคำสั่งที่ 2.7 ตัวอย่างของ onSaveInstanceState() และ onRestoreInstanceState()<br />

35<br />

@Override<br />

protected void onSaveInstanceState(Bundle outState) {<br />

super.onSaveInstanceState(outState);<br />

//save the relevant information<br />

outState.putString("name", localUserName);<br />

outState.putFloatArray("array", localFloatArray);<br />

}<br />

@Override<br />

public void onRestoreInstanceState(Bundle savedInstanceState) {<br />

super.onRestoreInstanceState(savedInstanceState);<br />

//restore the relevant information<br />

localUserName = savedInstanceState.getString("name");<br />

localFloatArray = savedInstanceState.getFloatArray("array");<br />

}<br />

ในคำสั่ง onCreate() จะมี Bundle savedInstanceState อยู่ด้วย ในกรณีที่แอคทิวิตี้เริ่ม<br />

ทำงานอีกครั้งหลังจากที่ก่อนหน้านี้ได้ปิดการทำงานของเครื่องไป ข้อมูลสถานะที่จัดเก็บจากคำสั่ง<br />

onSaveInstanceState() จะถูกส่งค่าไปยังคำสั่ง onCreate() ซึ่งค่าเหล่านี้จะใช้ร่วมกับคำสั่ง<br />

onRestoreInstanceState() เพื่อเรียกคืนสถานะเดิมให้แก่อินสแตนซ์<br />

มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

แอพจะประกอบด้วยแอคทิวิตี้อย่างน้อยหนึ่งอย่าง ในแอพที่ซับซ้อนจะประกอบด้วยการทำงาน<br />

ร่วมกันของแอคทิวิตี้หลายตัว ยกตัวอย่างแอพเกมจะมีแอคทิวิตี้ เช่น ส่วนของเกมและส่วนของการ<br />

แสดงคะแนน หรือในแอพประเภทโน้ตแพดก็จะประกอบด้วยแอคทิวิตี้ 3 อย่าง เช่น การดูลิสต์ของ<br />

โน้ต การอ่านโน้ตที่เลือกไว้ และการแก้ไขโน้ต<br />

แอคทิวิตี้หลักจะถูกประกาศไว้ในไฟล์ AndroidManifest ซึ่งแอคทิวิตี้นี้จะเริ่มทำงานเมื่อแอพ<br />

เริ่มทำงาน และแอคทิวิตี้สามารถเรียกใช้งานแอคทิวิตี้อื่นๆ ได้โดยใช้เหตุการณ์หรืออีเวนต์ต่างๆ มา<br />

กระตุ้น เมื่อแอคทิวิตี้ตัวที่ 2 ถูกเรียกใช้งาน แอคทิวิตี้หลักก็จะหยุด (Pause) ไว้ เมื่อแอคทิวิตี้ที่ 2<br />

ทำงานเสร็จแอคทิวิตี้หลักก็จะกลับมาทำงานต่อ


36 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

การเรียกใช้คอมโพเน็นต์ภายในแอพนั้น เราจะใช้อินเท็นต์ (Intent) ในการอ้างอิง ซึ่งเมื่อมีการ<br />

เรียกใช้อินเท็นต์ ระบบปฏิบัติการก็จะเรียกใช้คอมโพเน็นต์ที่เกี่ยวข้องขึ้นมาทำงาน แม้ว่าแอพที่เรียกใช้<br />

งานนั้นจะเป็นแอพภายนอกหรือแอพของระบบปฏิบัติการเองก็ตาม<br />

ระบบปฏิบัติการแอนดรอยด์จะพยายามใช้ประโยชน์จากการใช้งานอินเท็นต์ให้ได้มากที่สุดเพื่อ<br />

ให้ระบบมีประสิทธิภาพมากขึ้น มีลักษณะการทำงานเป็นโมดูล อย่างเช่น แอพที่แสดงสมุดโทรศัพท์<br />

ภายในเครื่อง พอได้ติดตั้งลงบนโทรศัพท์แล้ว เมื่อผู้ใช้เรียกใช้แอพและเลือกรายการติตต่อด้วย<br />

อินเท็นต์ ระบบปฏิบัติการแอนดรอยด์ก็จะค้นหาแอคทิวิตี้ที่เกี่ยวข้องกับอินเท็นต์นั้นเพื่อแสดงรายการ<br />

สมุดโทรศัพท์และกระบวนการต่างๆ โดยที่ผู้พัฒนาไม่จำเป็นต้องเขียนแอพในส่วนของการสืบค้นสมุด<br />

โทรศัพท์เลย<br />

กรรมวิธี: การใช้ปุ่ม (Button) และฟิลด์ข้อความ (TextView)<br />

ในส่วนนี้จะแสดงถึงการใช้งานมัลติเพิลแอคทิวิตี้ ซึ่งมีประโยชน์ในการทำงานแบบตอบสนองต่อ<br />

เหตุการณ์ โดยเราจะใช้ปุ่มมาแสดงการทำงานนี้ ขั้นตอนต่อจากนี้จะเป็นการเพิ่มปุ่มลงบนเลย์เอาต์<br />

และกำหนดการทำงานให้กับปุ่มเวลาที่ถูกกด<br />

1. ใส่ปุ่มลงในไฟล์เลย์เอาต์แบบ XML ดังนี้<br />

<br />

2. ประกาศค่า Button ID ให้กับปุ่มในไฟล์เลย์เอาต์<br />

Button startButton = (Button) findViewById(R.id.trigger);<br />

3. กำหนด Listener เพื่อตรวจจับการกดปุ่ม<br />

//setup button listener<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

//insert onClick here<br />

});<br />

4. กำหนดการทำงานให้กับฟังก์ชั่น onClick() เพื่อให้ทำงานต่างๆ ตามที่ต้องการ<br />

public void onClick(View view) {<br />

// do something here<br />

}<br />

การที่จะให้แสดงผลลัพธ์การทำงานให้ชัดเจนนั้น คุณต้องเปลี่ยนข้อความที่แสดงบนจอภาพ<br />

โดยขั้นตอนด้านล่างนี้เป็นการเขียนคำสั่งเพื่อประกาศกล่องข้อความและเปลี่ยนแปลงข้อความ<br />

1. กำหนดกล่องข้อความและค่า ID ลงไปในไฟล์เลย์เอาต์ XML และควรกำหนดค่าเริ่มต้น<br />

ให้แก่ค่าบางค่าด้วย (ในที่นี้เราจะกำหนดค่าเริ่มต้นให้ข้อความเป็นคำว่า “hello” ลงใน<br />

ไฟล์ string.xml)<br />


มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

37<br />

/><br />

android:layout_height=”wrap_content”<br />

android:text=”@string/hello”<br />

2. ประกาศ TextView และ TextView ID ในไฟล์เลย์เอาต์<br />

private TextView tv = (TextView) findViewById(R.id.hello_text);<br />

3. ถ้าต้องการเปลี่ยนข้อความให้ใช้ฟังก์ชั่น setText<br />

tv.setText(“new text string”);<br />

ตัวอย่างข้างต้นนี้เป็นเทคนิคที่ใช้ในส่วนของการติดต่อกับผู้ใช้งาน (UI) ซึ่งเราจะพูดถึงอีกครั้งใน<br />

บทที่ 4<br />

กรรมวิธี: การเรียกใช้งานแอคทิวิตี้จากอีเวนต์ต่างๆ<br />

ในส่วนนี้เราจะกำหนดให้ MenuScreen เป็นแอคทิวิตี้หลักตามชุดคำสั่งที่ 2.8 โดยจะทำการ<br />

เรียกใช้งานแอคทิวิตี้ชื่อ PlayGame และใช้วิดเจ็ต Button ในการตรวจจับเหตุการณ์ของการกดปุ่ม<br />

เมื่อผู้ใช้กดปุ่ม ฟังก์ชั่น startGame() จะเริ่มทำงาน และเมื่อผู้ใช้กดปุ่มในแอคทิวิตี้ PlayGame<br />

ฟังก์ชั่น finish() ก็จะถูกเรียกขึ้นมาใช้งานเพื่อคืนการทำงานกลับสู่แอคทิวิตี้หลัก โดยขั้นตอนในการ<br />

เรียกแอคทิวิตี้มีดังนี้<br />

1. ประกาศอินเท็นต์ที่อ้างอิงไปยังแอคทิวิตี้ที่จะเรียกใช้งาน<br />

2. เรียกใช้ startActivity จากอินเท็นต์นี้<br />

3. ประกาศแอคทิวิตี้เพิ่มเติมใน AndroidManifest<br />

ชุดคำสั่งที่ 2.8 src/com/cookbook/launch_activity/MenuScreen.java<br />

package com.cookbook.launch_activity;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

public class MenuScreen extends Activity {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

}<br />

//setup button listener<br />

Button startButton = (Button) findViewById(R.id.play_game);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

startGame();<br />

}<br />

});


38 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

}<br />

private void startGame() {<br />

Intent launchGame = new Intent(this, PlayGame.class);<br />

startActivity(launchGame);<br />

}<br />

การกำหนด Current Context ใน Anonymous Inner Class<br />

จากชุดคำสั่งที่ 2.8 มีการเรียกใช้แอคทิวิตี้จากการกดปุ่ม ซึ่งในที่นี้เราจะต้องกำหนด Context ให้แก่อินเท็นต์<br />

การใช้คำสั่ง this ในฟังก์ชั่น onClick() อาจจะไม่ชัดเจนนัก สำหรับวิธีต่างๆ ที่ใช้ในการกำหนด Current Context ใน<br />

Anonymous Inner Class มีดังนี้<br />

m ใช้คำสั่ง Context.this แทนคำสั่ง this<br />

m ใช้ getApplicationContext() แทนคำสั่ง this<br />

m ใช้ชื่อของคลาสว่า MenuScreen.this<br />

การเรียกใช้งานฟังก์ชั่นที่มีการกำหนดระดับของ Context อย่างเหมาะสมนั้น จะแสดงอยู่ในชุดคำสั่งที่ 2.8 ในส่วน<br />

ของฟังก์ชั่น startGame()<br />

ในชุดคำสั่งที่ 2.9 แอคทิวิตี้ชื่อ playGame จะประกอบด้วยปุ่มที่มีฟังก์ชั่น onClick() คอย<br />

ตรวจจับการกดปุ่ม ซึ่งจะเรียกใช้งานฟังก์ชั่น finish() เพื่อคืนการทำงานกลับไปสู่แอคทิวิตี้หลัก<br />

และคุณสามารถเพิ่มการทำงานอื่นๆ ลงไปในแอคทิวิตี้นี้ได้<br />

ชุดคำสั่งที่ 2.9 src/com/cookbook/launch_activity/PlayGame.java<br />

package com.cookbook.launch_activity;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

public class PlayGame extends Activity {<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.game);<br />

//setup button listener<br />

Button startButton = (Button) findViewById(R.id.end_game);<br />

startButton.setOnClickListener(new View.OnClickListener() {


มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

39<br />

}<br />

}<br />

public void onClick(View view) {<br />

finish();<br />

}<br />

});<br />

ชุดคำสั่งที่ 2.10 แสดงการเพิ่มปุ่มลงในเลย์เอาต์ main และเชื่อมโยงกับ ID ของ play_game<br />

ที่ได้ประกาศไว้ในชุดคำสั่งที่ 2.8 และกำหนดขนาดของปุ่มไว้ด้วย โดยจะใช้ค่าเป็น dip (device-independent<br />

pixel) ซึ่งคุณสามารถดูรายละเอียดเพิ่มเติมได้ในบทที่ 4<br />

ชุดคำสั่งที่ 2.10 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

ชุดคำสั่งที่ 2.11 แสดงไฟล์เลย์เอาต์ game.xml ที่มีการอ้างอิง end_game ID<br />

ชุดคำสั่งที่ 2.11 res/layout/game.xml<br />

<br />

<br />

<br />


40 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

ชุดคำสั่งที่ 2.12 แสดงถึงการกำหนดค่าคงที่แบบข้อความ ในที่นี้เรากำหนดค่าให้แก่สตริง<br />

(String) ชื่อ play_game และ end_game<br />

ชุดคำสั่งที่ 2.12 res/values/strings.xml<br />

<br />

<br />

This is the Main Menu<br />

LaunchActivity<br />

Play game?<br />

Done?<br />

<br />

ชุดคำสั่งที่ 2.13 แสดงการรีจีสเตอร์ (Register) แอ็กชั่นให้แก่คลาส PlayGame<br />

ชุดคำสั่งที่ 2.13 AndroidManifest.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />


มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

กรรมวิธี: การเรียกใช้งานแอคทิวิตี้เพื่อแสดงผลลัพธ์โดยการแปลงเสียง<br />

เป็นข้อความ<br />

ในส่วนนี้จะแสดงให้เห็นถึงการเรียกใช้แอคทิวิตี้เพื่อแสดงผลลัพธ์ รวมทั้งสาธิตการใช้งานระบบ<br />

การแปลงเสียงเป็นข้อความ ซึ่งพัฒนาโดย Google (RecognizerIntent) และแสดงผลลัพธ์ออก<br />

ทางจอภาพ โดยเราจะสั่งให้ RecognizerIntent เริ่มทำงานเมื่อมีการกดปุ่ม อินเท็นต์นี้จะทำการ<br />

ตรวจจับเสียงจากไมโครโฟนและแปลงข้อมูลเป็นข้อความ เมื่ออินเท็นต์ทำงานเสร็จแล้วก็จะส่งผ่าน<br />

ข้อความนี้กลับไปยังแอคทิวิตี้ที่เรียกใช้งานอินเท็นต์นี้<br />

ขั้นตอนก่อนที่จะส่งข้อมูลกลับนั้น ฟังก์ชั่น onActivityResult() จะถูกเรียกใช้พร้อมกับ<br />

ข้อมูลที่จะส่งกลับ และเรียกใช้ฟังก์ชั่น onResume() เพื่อให้แอคทิวิตี้มีสถานะเป็นปกติ ในบางครั้งการ<br />

เรียกใช้แอคทิวิตี้อาจจะพบปัญหาและไม่มีการส่งค่ากลับ ดังนั้นเราจะต้องตรวจสอบค่าของ resultcode<br />

เพื่อให้แน่ใจว่ามีค่าเป็น RESULT_OK ก่อนที่จะทำการส่งข้อมูลกลับ<br />

ขั้นตอนการเรียกใช้แอคทิวิตี้มีดังนี้<br />

1. เรียกใช้คำสั่ง startActivityForResult() กำหนดแอคทิวิตี้ที่ต้องการใช้งาน<br />

2. ทำการโอเวอร์ไรด์ฟังก์ชั่น onActivityResult() เพื่อตรวจสอบสถานะของผลลัพธ์<br />

ตรวจสอบ requestCode และส่งผ่านข้อมูลกลับ<br />

ขั้นตอนการใช้งาน RecognizerIntent มีดังนี้<br />

1. ประกาศการใช้งานอินเท็นต์ด้วยแอ็กชั่น ACTION_RECOGNIZE_SPEECH<br />

2. กำหนดค่าต่างๆ เพิ่มเติมให้แก่อินเท็นต์ เช่น EXTRA_LANGUAGE_MODEL ซึ่งกำหนด<br />

เป็นค่า LANGUAGE_MODEL_FREE_FORM หรือ LANGUAGE_MODEL_WEB_<br />

SEARCH<br />

3. ทำการส่งข้อมูลกลับ ซึ่งประกอบด้วยลิสต์ของข้อความที่แปลงมาจากเสียง โดยจะใช้<br />

คำสั่ง data.getStringArrayListExtra เพื่อดึงข้อมูลเหล่านี้<br />

ชุดคำสั่งที่ 2.14 ในส่วนของแอคทิวิตี้หลัก คำสั่ง TextView จะถูกใช้เพื่อแสดงข้อความบน<br />

จอภาพ<br />

นอกเหนือจากนี้ไฟล์ที่จำเป็นจะต้องใช้งานคือไฟล์ main.xml และ string.xml ซึ่งใช้เพื่อสร้าง<br />

Button และ TextView ส่วน AndroidManifest จะถูกประกาศในแอคทิวิตี้หลักเท่านั้น และอินเท็นต์<br />

ชื่อ RecognizerIntent เป็นแอคทิวิตี้ที่มีอยู่ในระบบปฏิบัติการแอนดรอยด์อยู่แล้ว เลยไม่จำเป็นต้อง<br />

ประกาศก่อนการใช้งาน<br />

41


42 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

ชุดคำสั่งที่ 2.14 src/com/cookbook/launch_for_result/RecognizerIntent Example.java<br />

package com.cookbook.launch_for_result;<br />

import java.util.ArrayList;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.speech.RecognizerIntent;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class RecognizerIntentExample extends Activity {<br />

private static final int RECOGNIZER_EXAMPLE = 1001;<br />

private TextView tv;<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.text_result);<br />

//setup button listener<br />

Button startButton = (Button) findViewById(R.id.trigger);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

// RecognizerIntent prompts for speech and returns text<br />

Intent intent =<br />

new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);<br />

}<br />

});<br />

}<br />

intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,<br />

RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);<br />

intent.putExtra(RecognizerIntent.EXTRA_PROMPT,<br />

"Say a word or phrase\nand it will show as text");<br />

startActivityForResult(intent, RECOGNIZER_EXAMPLE);<br />

@Override<br />

protected void onActivityResult(int requestCode,<br />

int resultCode, Intent data) {<br />

//use a switch statement for more than one request code check<br />

if (requestCode==RECOGNIZER_EXAMPLE && resultCode==RESULT_OK) {


มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

43<br />

// returned data is a list of matches to the speech input<br />

ArrayList result =<br />

data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);<br />

}<br />

//display on screen<br />

tv.setText(result.toString());<br />

}<br />

}<br />

super.onActivityResult(requestCode, resultCode, data);<br />

กรรมวิธี: การสร้างรายการตัวเลือก<br />

ในส่วนนี้จะแสดงถึงการสร้างรายการตัวเลือกหรือลิสต์ (List) เพื่อที่ผู้ใช้จะได้คลิกเพื่อเลือก<br />

ตัวเลือก โดยเราจะใช้แอคทิวิตี้ชื่อ ListActivity มาช่วยในการสร้างลิสต์<br />

ขั้นตอนของการสร้างลิสต์มีดังนี้<br />

1. สร้างคลาสเพื่อเรียกใช้งานคลาส ListActivity<br />

public class ActivityExample extends ListActivity {<br />

//content here<br />

}<br />

2. สร้างอาร์เรย์ (Array) ของข้อความเพื่อกำหนดลาเบลของลิสต์<br />

static final String[] ACTIVITY_CHOICES = new String[] {<br />

};<br />

“Action 1”,<br />

“Action 2”,<br />

“Action 3”<br />

3. เรียกใช้คำสั่ง setListAdapter() และ ArrayAdapter เพื่อกำหนดลิสต์และเลย์เอาต์<br />

setListAdapter(new ArrayAdapter(this,<br />

android.R.layout.simple_list_item_1, ACTIVITY_CHOICES));<br />

getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);<br />

getListView().setTextFilterEnabled(true);<br />

4. เรียกใช้ OnItemClickListener เพื่อตรวจสอบว่าตัวเลือกใดที่ถูกลือก<br />

getListView().setOnItemClickListener(new OnItemClickListener()<br />

{<br />

@Override<br />

public void onItemClick(AdapterView arg0, View arg1,<br />

int arg2, long arg3) {<br />

switch(arg2) {//extend switch to as many as needed<br />

case 0:<br />

//code for action 1


44 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

});<br />

}<br />

break;<br />

case 1:<br />

//code for action 2<br />

break;<br />

case 2:<br />

//code for action 3<br />

break;<br />

default: break;<br />

}<br />

เทคนิคนี้เราจะนำไปใช้งานในหัวข้อถัดไป<br />

กรรมวิธี: การใช้งานอินเท็นต์แบบ Implicit เพื่อสร้างแอคทิวิตี้<br />

อินเท็นต์ประเภท Implicit นั้น ไม่จำเป็นต้องระบุคอมโพเน็นต์ที่จะต้องใช้งาน เพราะมันถูก<br />

ควบคุมโดยฟิลเตอร์ ซึ่งระบบปฏิบัติการแอนดรอยด์จะทำการเลือกใช้คอมโพเน็นต์ที่เหมาะสมให้เอง<br />

การใช้งาน Intent Filter โดยมากแล้วจะเป็นประเภทแอ็กชั่น และแอ็กชั่นที่ใช้บ่อยๆ ก็คือ<br />

ACTION_VIEW ซึ่งจะต้องใช้ Uniform Resource Identifier (URI) เพื่อกำหนดและแสดงข้อมูลแก่<br />

ผู้ใช้งาน<br />

ขั้นตอนในการเรียกแอคทิวิตี้โดยใช้อินเท็นต์แบบ Implicit มีขั้นตอนดังนี้<br />

1. ประกาศการใช้งานอินเท็นต์และฟิลเตอร์ที่เกี่ยวข้อง (ACTION_VIEW, ACTION_WEB_<br />

SEARCH ฯลฯ)<br />

2. กำหนดข้อมูลอื่นๆ เพิ่มเติมให้แก่อินเท็นต์เพื่อให้เพียงพอต่อการสั่งให้แอคทิวิตี้ทำงาน<br />

3. ส่งผ่านอินเท็นต์นี้ไปยัง startActivity()<br />

ชุดคำสั่งที่ 2.5 จะแสดงให้เห็นถึงหลายๆ อินเท็นต์<br />

ชุดคำสั่งที่ 2.15 src/com/cookbook/implicit_intents/ListActivityExample.java<br />

package com.cookbook.implicit_intents;<br />

import android.app.ListActivity;<br />

import android.app.SearchManager;<br />

import android.content.Intent;<br />

import android.net.Uri;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.AdapterView;<br />

import android.widget.ArrayAdapter;


import android.widget.ListView;<br />

import android.widget.AdapterView.OnItemClickListener;<br />

มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

45<br />

public class ListActivityExample extends ListActivity {<br />

static final String[] ACTIVITY_CHOICES = new String[] {<br />

"Open Website Example",<br />

"Open Contacts",<br />

"Open Phone Dialer Example",<br />

"Search Google Example",<br />

"Start Voice Command"<br />

};<br />

final String searchTerms = "superman";<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setListAdapter(new ArrayAdapter(this,<br />

android.R.layout.simple_list_item_1, ACTIVITY_CHOICES));<br />

getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);<br />

getListView().setTextFilterEnabled(true);<br />

getListView().setOnItemClickListener(new OnItemClickListener()<br />

{<br />

@Override<br />

public void onItemClick(AdapterView arg0, View arg1,<br />

int arg2, long arg3) {<br />

switch(arg2) {<br />

case 0: //opens web browser and navigates to given website<br />

startActivity(new Intent(Intent.ACTION_VIEW,<br />

Uri.parse("http://www.android.com/")));<br />

break;<br />

case 1: //opens contacts application to browse contacts<br />

startActivity(new Intent(Intent.ACTION_VIEW,<br />

Uri.parse("content://contacts/people/")));<br />

break;<br />

case 2: //opens phone dialer and fills in the given number<br />

startActivity(new Intent(Intent.ACTION_VIEW,<br />

Uri.parse("tel:12125551212")));<br />

break;<br />

case 3: //search Google for the string<br />

Intent intent= new Intent(Intent.ACTION_WEB_SEARCH );<br />

intent.putExtra(SearchManager.QUERY, searchTerms);<br />

startActivity(intent);<br />

break;<br />

case 4: //starts the voice command<br />

startActivity(new<br />

Intent(Intent.ACTION_VOICE_COMMAND));


}<br />

46 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

}<br />

}<br />

});<br />

break;<br />

default: break;<br />

}<br />

กรรมวิธี: การส่งผ่านข้อมูลระหว่างแอคทิวิตี้<br />

ในบางครั้งเราอาจต้องการส่งข้อมูลไปยังแอคทิวิตี้ หรือบางครั้งต้องการส่งข้อมูลจากแอคทิวิตี้ที่<br />

ทำงานอยู่ กลับไปยังแอคทิวิตี้ที่เรียกใช้ ยกตัวอย่างเช่น ค่าคะแนนของเกมในตอนท้าย จะต้องถูกส่ง<br />

ผ่านไปยังหน้าจอค่าคะแนนสูงสุด ซึ่งวิธีการส่งค่าระหว่างแอคทิวิตี้นั้นมีหลายวิธีดังนี้<br />

m ประกาศตัวแปรในแอคทิวิตี้ที่ทำหน้าที่เรียก (ตัวอย่างเช่น int finalScore) และกำหนด<br />

ค่าในแอคทิวิตี้ที่ถูกเรียกใช้ (เช่น CallingActivity.finalScore=score)<br />

m กำหนดฟิลด์เพิ่มเติมลงในแอคทิวิตี้ (จะพูดถึงในหัวข้อนี้)<br />

m ใช้ Preference เพื่อเก็บข้อมูลที่จะเรียกใช้ในภายหลัง (จะพูดถึงในบทที่ 5)<br />

m ใช้ฐานข้อมูล SQLite เพื่อเก็บและส่งคืนข้อมูล (จะพูดถึงในบทที่ 9)<br />

ในส่วนนี้จะแสดงถึงการส่งผ่านข้อมูลจากแอคทิวิตี้หลักไปยังแอคทิวิตี้ที่ถูกเรียกใช้ ซึ่งใน<br />

แอคทิวิตี้นั้นอาจมีการเปลี่ยนแปลงข้อมูลและส่งกลับมายังแอคทิวิตี้หลัก<br />

ตัวแปร (ในที่นี้เป็นชนิดตัวเลขและข้อความ) จะถูกประกาศในแอคทิวิตี้ชื่อ StartScreen<br />

เมื่ออินเท็นต์ถูกสร้างขึ้นเพื่อเรียกใช้คลาส PlayGame ตัวแปรเหล่านี้จะถูกรวมไปไว้ในอินเท็นต์ด้วยการ<br />

ใช้เมธอด putExtra และเมื่อผลลัพธ์ถูกส่งกลับมาจากแอคทิวิตี้ ก็สามารถอ่านข้อมูลดังกล่าวได้ด้วย<br />

การใช้เมธอด getExtras<br />

ชุดคำสั่งที่ 2.16 src/com/cookbook/passing_data_activities/StartScreen.java<br />

package com.cookbook.passing_data_activities;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class StartScreen extends Activity {<br />

private static final int PLAY_GAME = 1010;


private TextView tv;<br />

private int meaningOfLife = 42;<br />

private String userName = "Douglas Adams";<br />

มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

47<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.startscreen_text);<br />

//display initial values<br />

tv.setText(userName + ":" + meaningOfLife);<br />

}<br />

//setup button listener<br />

Button startButton = (Button) findViewById(R.id.play_game);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

startGame();<br />

}<br />

});<br />

@Override<br />

protected void onActivityResult(int requestCode,<br />

int resultCode, Intent data) {<br />

if (requestCode == PLAY_GAME && resultCode == RESULT_OK) {<br />

meaningOfLife = data.getExtras().getInt("returnInt");<br />

userName = data.getExtras().getString("userName");<br />

//show it has changed<br />

tv.setText(userName + ":" + meaningOfLife);<br />

}<br />

super.onActivityResult(requestCode, resultCode, data);<br />

}<br />

private void startGame() {<br />

Intent launchGame = new Intent(this, PlayGame.class);<br />

//passing information to launched activity<br />

launchGame.putExtra("meaningOfLife", meaningOfLife);<br />

launchGame.putExtra("userName", userName);<br />

}<br />

}<br />

startActivityForResult(launchGame, PLAY_GAME);


48 บทที่ 2 การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์<br />

ค่าของตัวแปรจะถูกส่งไปยังแอคทิวิตี้ PlayGame ซึ่งสามารถอ่านค่าได้โดยใช้เมธอด<br />

getIntExtra และ getStringExtra เมื่อแอคทิวิตี้ทำงานเสร็จ และจัดเตรียมอินเท็นต์เพื่อส่งค่า<br />

กลับ คำสั่ง putExtra ก็สามารถส่งค่ากลับไปยังแอคทิวิตี้ที่ทำหน้าที่เรียกใช้อยู่ได้ด้วย ขั้นตอนพวกนี้<br />

จะแสดงให้เห็นในชุดคำสั่งที่ 2.17<br />

ชุดคำสั่งที่ 2.17 src/com/cookbook/passing_data_activities/PlayGame.java<br />

package com.cookbook.passing_data_activities;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class PlayGame extends Activity {<br />

private TextView tv2;<br />

int answer;<br />

String author;<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.game);<br />

tv2 = (TextView) findViewById(R.id.game_text);<br />

//reading information passed to this activity<br />

//Get the intent that started this activity<br />

Intent i = getIntent();<br />

//returns -1 if not initialized by calling activity<br />

answer = i.getIntExtra("meaningOfLife", -1);<br />

//returns [] if not initialized by calling activity<br />

author = i.getStringExtra("userName");<br />

tv2.setText(author + ":" + answer);<br />

//change values for an example of return<br />

answer = answer - 41;<br />

author = author + " Jr.";<br />

//setup button listener<br />

Button startButton = (Button) findViewById(R.id.end_game);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

//return information to calling activity


มัลติเพิลแอคทิวิตี้ (Multiple Activities)<br />

49<br />

}<br />

}<br />

});<br />

}<br />

Intent i = getIntent();<br />

i.putExtra("returnInt", answer);<br />

i.putExtra("returnStr", author);<br />

setResult(RESULT_OK, i);<br />

finish();


50 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน


51<br />

บทที่ 3<br />

เธรด เซอร์วิส รีซีฟเวอร์<br />

และการแจ้งเตือน<br />

ในบทนี้คุณจะได้ศึกษาเกี่ยวกับการออกแบบโครงสร้างของแอพ โดยเราจะอธิบายถึงการใช้<br />

คำสั่งเธรด (Thread) คำสั่งนี้เป็นคำสั่งที่ใช้ควบคุมการทำงานของงานย่อยต่างๆ ที่เซอร์วิส (Service)<br />

และรีซีฟเวอร์ (Receiver) ก็สามารถใช้งานร่วมกับเธรดได้ แอพประเภทวิดเจ็ตมีการนำเธรดเข้าไป<br />

ช่วยการทำงาน ซึ่งจะช่วยให้นักพัฒนาสามารถสร้างการทำงานแบบแจ้งเตือนได้หลายอย่าง<br />

เธรด (Thread)<br />

การทำงานของทุกๆ แอพนั้น โดยพื้นฐานแล้วจะทำงานแบบงานเดี่ยวหรือ Single Task ซึ่งจะ<br />

ทำงานไปเรื่อยๆ ตามลำดับของคำสั่ง และเพื่อเป็นการหลีกเลี่ยงเหตุการณ์จอภาพค้างเวลาที่ต้อง<br />

ทำงานเป็นเวลานาน เช่น การดาวน์โหลดข้อมูลหรือการคำนวณค่าที่ซับซ้อน เราจะกำหนดให้งานพวก<br />

นี้ทำงานแบบเบื้องหลัง โดยผู้เขียนแอพจะเป็นผู้กำหนดว่างานใดควรทำเบื้องหน้า และงานใดควรทำ<br />

เบื้องหลัง ซึ่งระบบปฏิบัติการแอนดรอยด์จะมีบทบาทในการจัดลำดับความสำคัญของงานที่จะทำ<br />

แอพในปัจจุบันจะใช้เธรดเข้ามาช่วยเพิ่มประสิทธิภาพในการทำงาน บางครั้งถ้าแอพทำงาน<br />

ผิดพลาด เกิดอาการค้าง จอภาพก็จะแสดงข้อความแจ้งเตือน ดังรูปที่ 3.1<br />

กรรมวิธี: การเรียกใช้งานเธรดลำดับที่ 2<br />

ในส่วนนี้คุณจะได้เห็นการทำงานเวลาที่กดปุ่มบนจอภาพแล้ว แอพจะเล่นเสียงเรียกเข้า<br />

(Ringtone) โดยเราจะสาธิตให้เห็นการทำงานของแอพเวลาที่ทำงานที่ต้องใช้เวลาในการประมวลผล<br />

โดยในชุดคำสั่งด้านล่างนี้จะเรียกใช้ฟังก์ชั่น play_music() เพื่อให้ทำงานโดยไม่มีการแยกเธรด<br />

ย่อยในระหว่างที่แอพกำลังเล่นเสียงเรียกเข้า<br />

Button startButton = (Button) findViewById(R.id.trigger);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view){<br />

// BAD USAGE: function call to time-consuming<br />

// function causes main thread to hang


52 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

});<br />

}<br />

play_music();<br />

รูปที่ 3.1 ตัวอย่างของข้อความเตือนเมื่อมีการทำางานของเธรดค้าง<br />

เหตุการณ์ในทำนองนี้จะเกิดขึ้นเมื่อผู้ใช้กดปุ่ม Back เพื่อกลับสู่หน้า Home หรือกดปุ่มบน<br />

จอภาพในระหว่างที่แอพกำลังเล่นเสียงอยู่ ซึ่งการกระทำเหล่านี้จะทำให้เกิดการแจ้งเตือนดังรูปข้างบน<br />

ได้<br />

เราจะแก้ไขปัญหาเหล่านี้โดยการสร้างเธรดที่ 2 ขึ้นมาเพื่อเรียกใช้ฟังก์ชั่น play_music() ซึ่งมี<br />

ขั้นตอนดังนี้<br />

1. สร้างเธรดขึ้นมาใหม่เพื่อรองรับออบเจ็กต์ Runnable<br />

Thread initBkgdThread = new Thread(<br />

//insert runnable object here<br />

);<br />

2. สร้างออบเจ็กต์ Runnable เพื่อโอเวอร์ไรด์เมธอด run() ที่จะใช้เรียกฟังก์ชั่นการทำงาน<br />

ที่ต้องการจะแตกเธรด<br />

new Runnable() {<br />

public void run() {<br />

play_music();<br />

}<br />

}<br />

3. เริ่มต้นการทำงานของเธรดที่จะสั่งให้ทำงาน<br />

initBkgdThread.start();


เธรด (Thread)<br />

ในเธรดที่ 2 ที่สร้างขึ้นในเธรดหลักนั้น เราจะกำหนดให้ทำงานที่ต้องใช้ระยะเวลา และใน<br />

ระหว่างที่เธรดที่ 2 กำลังทำงานอยู่นั้น เธรดหลักก็จะยังทำงานอื่นๆ ต่อไปได้<br />

ก่อนที่จะไปดูชุดคำสั่งสำหรับแอคทิวิตี้แบบเต็มๆ เรามาทำความเข้าใจเกี่ยวกับไฟล์สนับสนุนกัน<br />

ก่อนดีกว่า โดยเราจะสร้างเสียงเรียกเข้าโดยใช้ภาษา RTTTL (Ring-tone Text Transfer Language)<br />

คำสั่ง RTTTL ในที่นี้จะสร้างเสียงของตัวโน้ต A ซึ่งมีความถี่ของเสียงที่ 220 Hz เราจะใส่คำสั่งนี้ 1<br />

บรรทัดลงในไฟล์ และเก็บไว้ในไดเร็กทอรี่ res\raw\ และกำหนดชื่อรีซอร์สเป็น R.raw.a4<br />

ชุดคำสั่งที่ 3.1 RTTTL file res/raw/a4.rtttl, จะเล่นเสียงตัวโน้ต A (220 Hz) ที่มีระดับเสียงต่ำ<br />

กว่าเสียงตัวโน้ต C กลางมาตรฐาน (261 Hz)<br />

53<br />


54 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

สิ่งหนึ่งที่ควรคำนึงถึงเมื่อมีการใช้งานเธรดย่อยก็คือ เมื่อเราสั่งให้เธรดย่อยทำงานไปแล้ว<br />

เธรดย่อยก็จะทำงานต่อไปเรื่อยๆ แม้ว่าเธรดหลักจะหยุดการทำงานก็ตาม โดยเมื่อเรากดปุ่ม Back<br />

เพื่อกลับไปยังหน้าจอ Home ในระหว่างที่กำลังเล่นเสียงอยู่ เสียงที่เล่นอยู่นั้นก็จะยังเล่นต่อเนื่องไปจน<br />

จบ เหตุการณ์นี้เกิดขึ้นเพราะว่าเรายังไม่ได้กำหนดค่าให้แก่ฟังก์ชั่น play_music เพื่อตรวจสอบค่า<br />

แฟล็ก (Flag) (ในที่นี้ก็คือสถานะ paused) ซึ่งค่านี้ถูกกำหนดจากฟังก์ชั่น onPause() ของแอคทิวิตี้<br />

หลัก เราจะใช้ค่าแฟล็กนี้เพื่อหยุดการเล่นเสียงเมื่อเธรดหลักหยุดทำงาน<br />

การทำงานในข้างต้นนี้จะรวมอยู่ในแอคทิวิตี้ชื่อ PressAndPlay ดังชุดคำสั่งที่ 3.3<br />

ชุดคำสั่งที่ 3.3 src/com/cookbook/launch_thread/PressAndPlay.java<br />

package com.cookbook.launch_thread;<br />

import android.app.Activity;<br />

import android.media.MediaPlayer;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

public class PressAndPlay extends Activity {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Button startButton = (Button) findViewById(R.id.trigger);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view){<br />

}<br />

});<br />

}<br />

//standalone play_music() function call causes<br />

//main thread to hang. Instead, create<br />

//separate thread for time-consuming task<br />

Thread initBkgdThread = new Thread(new Runnable() {<br />

public void run() {<br />

play_music();<br />

}<br />

});<br />

initBkgdThread.start();<br />

int[] notes = {R.raw.c5, R.raw.b4, R.raw.a4, R.raw.g4};<br />

int NOTE_DURATION = 400; //millisec<br />

MediaPlayer m_mediaPlayer;<br />

private void play_music() {


}<br />

เธรด (Thread)<br />

for(int ii=0; ii


56 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

ปุ่มกดที่ใช้ในชุดคำสั่งนี้จะมีหน้าที่เดียวกับการทำงานในชุดคำสั่งอันที่แล้ว ซึ่งจะยังแสดงปุ่มบน<br />

จอภาพ ในขณะที่ฟังก์ชั่น detectEdge() ยังคงทำงานอยู่เบื้องหลังเหมือนเดิม<br />

ชุดคำสั่งที่ 3.4 src/com/cookbook/runnable_activity/EdgeDetection.java<br />

package com.cookbook.runnable_activity;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class EdgeDetection extends Activity implements Runnable {<br />

int numberOfTimesPressed=0;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

final TextView tv = (TextView) findViewById(R.id.text);<br />

//in-place function call causes main thread to hang:<br />

/* detectEdges(); */<br />

//instead, create background thread for time-consuming task<br />

Thread thread = new Thread(EdgeDetection.this);<br />

thread.start();<br />

Button startButton = (Button) findViewById(R.id.trigger);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view){<br />

}<br />

});<br />

}<br />

tv.setText("Pressed button " + ++numberOfTimesPressed<br />

+ " times\nAnd computation loop at "<br />

+ "(" + xi + ", " + yi + ") pixels");<br />

@Override<br />

public void run() {<br />

detectEdges();<br />

}


Edge Detection<br />

int xi, yi;<br />

private double detectEdges() {<br />

int x_pixels = 4000;<br />

int y_pixels = 3000;<br />

double image_transform=0;<br />

เธรด (Thread)<br />

57<br />

}<br />

}<br />

//double loop over pixels for image processing<br />

//meaningless hyperbolic cosine emulates time-consuming task<br />

for(xi=0; xi


58 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

//use when initially starting a thread<br />

myThread.setDaemon(true);<br />

myThread.start();<br />

วิธีสุดท้ายที่ใช้ในการยกเลิกการทำงานของเธรด คือการใช้คำสั่ง while(stillRunning)<br />

ในเมธอด run() และใช้คำสั่ง stillRunning=false เพื่อยกเลิกการทำงานของเธรด<br />

กรรมวิธี: การเรียกใช้งานเธรดร่วมกันระหว่างแอพ<br />

จากกรรมวิธีก่อนหน้านี้ คุณสามารถนำไปประยุกต์ในการเขียนแอพเพื่อใช้งานเธรดหลายๆ เธรด<br />

ภายใต้แอพเดียวกันได้ และในทางกลับกัน คุณสามารถใช้งานเธรดเพียงเธรดเดียวร่วมกับแอพหลายๆ<br />

ตัวได้ด้วย อย่างเช่นว่าถ้าคุณมีแอพอยู่ 2 ตัวที่ต้องการสื่อสารข้อมูลระหว่างกัน ก็ให้ใช้โปรโตคอล<br />

Inter-Process Communication (IPC) ในการทำงาน ตามขั้นตอนนี้<br />

1. กำหนดให้แอพที่จะทำงานร่วมกันมีการลงทะเบียนแพ็คเกจด้วยคีย์เดียวกันเพื่อเหตุผล<br />

ด้านความปลอดภัย<br />

2. กำหนดให้แอพทำงานด้วย User ID เดียวกันด้วยการประกาศค่าแอททริบิวต์<br />

android:sharedUserId=”my.shared.userid” ซึ่งอยู่ในไฟล์ AcitivityManifest.<br />

xml ของแต่ละแอพ<br />

3. ประกาศให้แอคทิวิตี้หรือคอมโพเน็นต์นั้นทำงานอยู่ภายใต้กระบวนการเดียวกันด้วยการ<br />

ประกาศค่าแอททริบิวต์ android:process=”my.shared.processname” ซึ่งอยู่ในไฟล์<br />

AcitivityManifest.xml ของแต่ละแอพให้มีค่าเดียวกัน<br />

ขั้นตอนเหล่านี้เป็นขั้นตอนง่ายๆ ที่ใช้ในการกำหนดเพื่อให้แน่ใจว่าคอมโพเน็นต์ทั้ง 2 ตัวทำงาน<br />

อยู่ภายใต้เธรดเดียวกัน และมีการแชร์ข้อมูลร่วมกัน สำหรับรายละเอียดขั้นตอนการทำงานและสิทธิ์<br />

ต่างๆ ในการแชร์ข้อมูลนั้น เราจะมาพูดถึงอีกครั้งในบทที่ 11 “เทคนิคขั้นสูงสำหรับพัฒนาแอพบน<br />

แอนดรอยด์”<br />

การส่งข้อมูลระหว่างเธรด : แฮนด์เลอร์ (Handler)<br />

หลังจากที่ได้รู้เกี่ยวกับการสั่งให้เธรดทำงานร่วมกันระหว่างเธรดหลักและเธรดย่อยกันไปแล้ว<br />

ทีนี้เรามากำหนดให้เธรดทั้ง 2 สามารถส่งผ่านข้อมูลร่วมกันได้บ้าง ซึ่งตัวอย่างการทำงานประเภทนี้ก็<br />

อย่างเช่น<br />

m เธรดหลักทำงานที่เกี่ยวข้องกับเวลา ต้องการความรวดเร็ว และมีการส่งข้อมูลไปประมวล<br />

ผลยังเธรดย่อยซึ่งต้องใช้ระยะเวลาประมวลผลนาน<br />

m มีการประมวลผลข้อมูลขนาดใหญ่ ซึ่งในระหว่างการประมวลผลมีการส่งข้อมูลที่ประมวล<br />

ผลแล้วบางส่วนกลับไปยังเธรดเพื่อแสดงผล<br />

การทำงานเช่นนี้เราจะใช้แฮนด์เลอร์ (Handler) มาช่วย ซึ่งเป็นออบเจ็กต์ที่ใช้ในการส่งข้อมูล<br />

ระหว่างเธรด โดยที่แฮนด์เลอร์แต่ละตัวจะถูกรวมเข้ากับเธรดแบบซิงเกิลเพื่อส่งผ่านข้อมูลเข้าสู่เธรดนี้<br />

และทำงานตามคำสั่ง


การส่งข้อมูลระหว่างเธรด: แฮนด์เลอร์ (Handler)<br />

กรรมวิธี: การใช้เธรดหลักเพื่อกำหนดช่วงเวลาในการทำงานของทาสก์<br />

ตอนนี้เราจะมาสร้างนาฬิกาจับเวลา ซึ่งมักถูกนำไปใช้ในหลายๆ แอพกัน ยกตัวอย่างเช่น ใช้ใน<br />

แอพเกมเพื่อจับเวลาในการเล่นเกมของแต่ละฉากและแสดงการจับเวลาบนอยู่จอภาพ เป็นต้น<br />

นาฬิกาจับเวลาจะทำงานอยู่ในเธรดย่อยแบบเบื้องหลัง ดังนั้นมันจะไม่ถูกขัดจังหวะการทำงาน<br />

จากเธรดหลัก แต่ทุกครั้งที่เวลามีการเปลี่ยนแปลง ตัวมันเองจะต้องส่งค่ากลับไปอัพเดตการแสดงผลที่<br />

จอภาพด้วย อย่างในชุดคำสั่งที่ 3.5 TextView จะเริ่มต้นทำงานโดยมีค่าเป็นคำกล่าวต้อนรับ<br />

และข้อความบนปุ่มที่มี ID เป็น trigger จะแสดงข้อความบนปุ่มเป็นคำว่า “Press Me”<br />

ชุดคำสั่งที่ 3.5 res/layout/main.xml<br />

59<br />

<br />

<br />

<br />

<br />

<br />

รีซอร์สข้อความในไฟล์ XML จะเกี่ยวข้องกับตัวแปร TextView ในจาวาแอคทิวิตี้ชื่อ BackgroundTimer<br />

ซึ่งจะใช้ค่าเริ่มต้นดังนี้<br />

mTimeLabel = (TextView) findViewById(R.id.text);<br />

mButtonLabel = (TextView) findViewById(R.id.trigger);<br />

นอกจากการกำหนดค่าของข้อความในจาวาแล้ว เรายังสามารถเปลี่ยนแปลงค่าดังกล่าวใน<br />

ระหว่างที่แอพกำลังทำงานอยู่ได้เช่นกัน เมื่อแอพเริ่มทำงาน mUpdateTimeTask จะเริ่มต้นจับเวลา<br />

และแก้ไขข้อมูลใน mTimeLabel ด้วยค่าของนาทีและวินาทีในขณะนั้น และเมื่อปุ่มถูกกด เมธอด<br />

onClick() ก็จะแก้ไขข้อความของ mButtonLabel ด้วยจำนวนครั้งของการกดปุ่ม<br />

ในขณะเดียวกันแฮนด์เลอร์ชื่อ mHandler ก็จะถูกสร้างขึ้นและใช้ลำดับการทำงานของ<br />

mUpdateTimeTask ซึ่งถูกเรียกใช้งานครั้งแรกในเมธอด onCreate() และจะทำงานแบบเรียกตัวแอง<br />

(Recursive) ไปเรื่อยๆ เพื่ออัพเดตเวลาทุกๆ 200 มิลลิวินาที ซึ่งค่านี้จะให้การแสดงผลดูราบรื่นกว่า<br />

การกำหนดระยะเวลาอัพเดตเป็น 1 วินาที คุณสามารถดูตัวอย่างของแอคทิวิตี้นี้ได้ในชุดคำสั่งที่ 3.6


60 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

ชุดคำสั่งที่ 3.6 src/com/cookbook/background_timer/BackgroundTimer.java<br />

package com.cookbook.background_timer;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.os.Handler;<br />

import android.os.SystemClock;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class BackgroundTimer extends Activity {<br />

//keep track of button presses, a main thread task<br />

private int buttonPress=0;<br />

TextView mButtonLabel;<br />

//counter of time since app started, a background task<br />

private long mStartTime = 0L;<br />

private TextView mTimeLabel;<br />

//Handler to handle the message to the timer task<br />

private Handler mHandler = new Handler();<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

if (mStartTime == 0L) {<br />

mStartTime = SystemClock.uptimeMillis();<br />

mHandler.removeCallbacks(mUpdateTimeTask);<br />

mHandler.postDelayed(mUpdateTimeTask, 100);<br />

}<br />

mTimeLabel = (TextView) findViewById(R.id.text);<br />

mButtonLabel = (TextView) findViewById(R.id.trigger);<br />

}<br />

Button startButton = (Button) findViewById(R.id.trigger);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view){<br />

mButtonLabel.setText("Pressed " + ++buttonPress<br />

+ " times");<br />

}<br />

});


การส่งข้อมูลระหว่างเธรด: แฮนด์เลอร์ (Handler)<br />

61<br />

private Runnable mUpdateTimeTask = new Runnable() {<br />

public void run() {<br />

final long start = mStartTime;<br />

long millis = SystemClock.uptimeMillis() - start;<br />

int seconds = (int) (millis / 1000);<br />

int minutes = seconds / 60;<br />

seconds = seconds % 60;<br />

};<br />

}<br />

mTimeLabel.setText("" + minutes + ":"<br />

+ String.format("%02d",seconds));<br />

mHandler.postDelayed(this, 200);<br />

@Override<br />

protected void onPause() {<br />

mHandler.removeCallbacks(mUpdateTimeTask);<br />

super.onPause();<br />

}<br />

}<br />

@Override<br />

protected void onResume() {<br />

super.onResume();<br />

mHandler.postDelayed(mUpdateTimeTask, 100);<br />

}<br />

กรรมวิธี: การใช้งานนาฬิกาจับเวลาถอยหลัง<br />

ในหัวข้อก่อนหน้านี้เป็นการใช้งานแฮนด์เลอร์เพื่อสร้างนาฬิกาจับเวลา คลาส CountDown-<br />

Timer ก็เป็นคลาสหนึ่งที่มีการวมการทำงานของเธรดย่อยแบบเบื้องหลังและแฮนด์เลอร์เพื่อทำหน้าที่<br />

นับเวลาถอยหลัง<br />

นาฬิกาจับเวลาถอยหลังจะประกอบด้วยอาร์กิวเมนต์(Argument) จำนวน 2 ตัว คือ จำนวนของ<br />

มิลลิวินาทีที่จะให้นาฬิกานับถอยหลัง และค่าช่วงเวลาที่จะใช้เป็นความถี่ในการเรียกใช้ค ำสั่ง onTick()<br />

ซึ่งคำสั่ง onTick() เป็นคำสั่งที่ใช้ในการอัพเดตข้อความที่แสดงการนับถอยหลัง ซึ่งสามารถดูได้ในชุด<br />

คำสั่งที่ 3.7<br />

ชุดคำสั่งที่ 3.7 src/com/cookbook/countdown/CountDownTimerExample.java<br />

package com.cookbook.countdown;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.os.CountDownTimer;<br />

import android.view.View;<br />

import android.widget.Button;


62 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

import android.widget.TextView;<br />

public class CountDownTimerExample extends Activity {<br />

//keep track of button presses, a main thread task<br />

private int buttonPress=0;<br />

TextView mButtonLabel;<br />

//count down timer, a background task<br />

private TextView mTimeLabel;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

mTimeLabel = (TextView) findViewById(R.id.text);<br />

mButtonLabel = (TextView) findViewById(R.id.trigger);<br />

new CountDownTimer(30000, 1000) {<br />

public void onTick(long millisUntilFinished) {<br />

mTimeLabel.setText(“seconds remaining: “<br />

+ millisUntilFinished / 1000);<br />

}<br />

public void onFinish() {<br />

mTimeLabel.setText(“done!");<br />

}<br />

}.start();<br />

}<br />

}<br />

Button startButton = (Button) findViewById(R.id.trigger);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view){<br />

mButtonLabel.setText("Pressed " + ++buttonPress + " times");<br />

}<br />

});<br />

กรรมวิธี: การแสดงผลในระหว่างที่แอพเริ่มทำงาน<br />

มาดูการทำงานต่างๆ ในช่วงที่แอพกำลังเริ่มทำงานกัน โดยเราจะนำข้อมูลเลย์เอาต์ที่กำหนดไว้<br />

ในไฟล์ loading.xml มาแสดงเป็นข้อความ “Loading…” ในระหว่างที่แอพเริ่มทำงาน คุณสามารถดู<br />

รายละเอียดของไฟล์ loading.xml ในชุดคำสั่งที่ 3.8 การแสดงผลนี้คุณจะกำหนดให้เป็นภาพโลโก้<br />

หรือภาพเคลื่อนไหวใดๆ ก็ได้


การส่งข้อมูลระหว่างเธรด: แฮนด์เลอร์ (Handler)<br />

63<br />

ชุดคำสั่งที่ 3.8 res/layout/loading.xml<br />

<br />

<br />

<br />

<br />

ในขณะที่เลย์เอาต์กำลังแสดงผลอยู่ ฟังก์ชั่น initializeArrays() ซึ่งภายในจะมีคำสั่งที่ต้อง<br />

ใช้ในการทำงานนั้นจะทำงานบนเธรดย่อยเบื้องหลังเพื่อเลี่ยงอาการจอภาพค้าง<br />

เมื่อการทำงานของฟังก์ชั่น initializeArrays() เสร็จสิ้น ก็จะมีการส่งข้อมูลไปยัง mHandler<br />

ซึ่งข้อมูลที่ส่งนั้นจะเป็นข้อมูลต่างๆ ที่ใช้ทำงานลำดับถัดไป โดยการส่งค่าข้อมูลว่างนั้น เราใช้คำสั่ง<br />

mHandler.sendEmptyMessage(0)<br />

การรับข้อมูลที่ส่งมานั้น เธรดหลักจะรันเมธอด handleMessage() ซึ่งจะโอเวอร์ไรด์คำสั่งถัดไป<br />

ที่จะทำงาน หลังจากที่ฟังก์ชั่น initializeArrays() ทำงานเสร็จ ในที่นี้งานลำดับถัดไปก็คือการ<br />

แสดงจอภาพจากไฟล์เลย์เอาต์ main.xml ตามรายละเอียดการทำงานในไฟล์ HandleMessage.java<br />

ในชุดคำสั่งที่ 3.9<br />

ชุดคำสั่งที่ 3.9 src/com/cookbook/handle_message/HandleMessage.java<br />

package com.cookbook.handle_message;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.os.Handler;<br />

import android.os.Message;<br />

public class HandleMessage extends Activity implements Runnable {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.loading);<br />

}<br />

Thread thread = new Thread(this);<br />

thread.start();


64 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

private Handler mHandler = new Handler() {<br />

public void handleMessage(Message msg) {<br />

setContentView(R.layout.main);<br />

}<br />

};<br />

public void run(){<br />

initializeArrays();<br />

mHandler.sendEmptyMessage(0);<br />

}<br />

final static int NUM_SAMPS = 1000;<br />

static double[][] correlation;<br />

void initializeArrays() {<br />

if(correlation!=null) return;<br />

}<br />

}<br />

correlation = new double[NUM_SAMPS][NUM_SAMPS];<br />

//calculation<br />

for(int k=0; k


เซอร์วิส<br />

65<br />

เซอร์วิสเริ่มทำงานโดย<br />

เมธอด startService()<br />

เซอร์วิสถูกสร้างโดย<br />

เมธอด bindService()<br />

เมธอด onCreate() ทำงาน<br />

เมธอด onCreate() ทำงาน<br />

เมธอด onStart() ทำงาน<br />

เมธอด onBind() ทำงาน<br />

เซอร์วิสกำลัง<br />

ทำงานอยู่<br />

เซอร์วิสถูกเรียกใช้งานโดย<br />

โปรแกรมลูกข่าย<br />

เมธอด onRebind() ทำงาน<br />

เซอร์วิสหยุดทำงาน<br />

เมธอด onUnbind() ทำงาน<br />

เมธอด onDestroy() ทำงาน<br />

เมธอด onDestroy() ทำงาน<br />

เซอร์วิสถูกยกเลิก<br />

เซอร์วิสถูกยกเลิก<br />

จะเห็นว่างานที่ทำอยู่เบื้องหลังภายใต้คอมโพเน็นต์ใดๆ จะหยุดการทำงานเมื่อคอมโพเน็นต์นั้น<br />

ถูกยกเลิก<br />

เซอร์วิสทุกๆ ตัวจะสร้างอินสแตนซ์มาจากคลาส Service หรือซับคลาสอื่นๆ ที่อยู่ภายใต้คลาส<br />

Service ซึ่งในแต่ละเซอร์วิสก็จะมีเมธอด onCreate() เช่นกัน โดยเซอร์วิสจะทำงานแบบ Start<br />

และ Stop เท่านั้น ไม่มีการ Pause แต่ก็สั่งให้หยุดการทำงานเซอร์วิสนั้นได้ด้วยเมธอด onDestroy()<br />

รูปที่ 3.2 วงจรการทำางานของเซอร์วิส จาก http://developer.android.com/ .<br />

กรรมวิธี: การสร้างเซอร์วิส<br />

ขั้นตอนในการสร้างเซอร์วิสในคอมโพเน็นต์ มีดังนี้<br />

1. สร้างคลาสจากอินสแตนซ์ Service (ในโปรแกรม Eclipse สามารถทำได้โดยคลิกขวาที่<br />

โปรเจ็กต์ และเลือกเมนู New → Class และกำหนดค่าของซูเปอร์คลาสเป็น<br />

android.app.Service)


66 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

2. ประกาศเซอร์วิสในไฟล์ AndroidManifest.xml โดยการเพิ่มคำสั่งนี้<br />

<br />

3. โอเวอร์ไรด์เมธอด onCreate() และ onDestroy() (ในโปรแกรม Eclipse สามารถ<br />

ทำได้โดยคลิกขวาที่คลาส และเลือกเมนู Source → Override/Implement<br />

Methods) ซึ่งเมธอดเหล่านี้จะทำงานเมื่อเซอร์วิสเริ่มหรือหยุดทำงาน<br />

4. โอเวอร์ไรด์เมธอด onBind() ที่จะทำงานเมื่อมีการเชื่อมโยงระหว่างเซอร์วิสกับ<br />

คอมโพเน็นต์<br />

5. สั่งให้เซอร์วิสเริ่มทำงานโดยใช้คำสั่งจากภายนอกมาสั่งการ เพราะเซอร์วิสไม่สามารถเริ่ม<br />

การทำงานด้วยตัวเองได้<br />

เราจะใช้ฟังก์ชั่น play_music() จากกรรมวิธีอันแรกสุดของบทนี้มาสั่งให้ทำงานแบบเซอร์วิส<br />

ตามรายละเอียดในรูปก่อนหน้านี้ตามชุดคำสั่งที่ 3.10 ซึ่งมีการทำงานดังนี้<br />

m คำสั่ง Toast จะใช้ในการแสดงข้อมูลเมื่อเซอร์วิสเริ่มหรือหยุดการทำงาน<br />

m โอเวอร์ไรด์เมธอด onBind() แต่ไม่มีการใช้งานใดๆ<br />

m นำคำสั่งเธรดมาใช้งานเพื่อให้เวลาที่เล่นไฟล์เพลงแล้วจะไม่ไปหยุดการทำงานอื่นๆ ของ<br />

จอภาพ<br />

m เมื่อแอคทิวิตี้ถูกยกเลิก เซอร์วิสจะไม่หยุดการทำงาน (อย่างเช่นการหมุนจอภาพแล้วจะไม่<br />

ส่งผลกับการเล่นไฟล์เพลง) จะเห็นได้ว่าเซอร์วิสถูกสั่งให้เริ่มทำงานโดยแอคทิวิตี้ แต่การ<br />

ทำงานที่เกิดขึ้นภายในเซอร์วิสนั้นจะถูกควบคุมโดยเซอร์วิสเอง<br />

ชุดคำสั่งที่ 3.10 src/com/cookbook/simple_service/SimpleService.java<br />

package com.cookbook.simple_service;<br />

import android.app.Service;<br />

import android.content.Intent;<br />

import android.media.MediaPlayer;<br />

import android.os.IBinder;<br />

import android.widget.Toast;<br />

public class SimpleService extends Service {<br />

@Override<br />

public IBinder onBind(Intent arg0) {<br />

return null;<br />

}<br />

boolean paused = false;


@Override<br />

public void onCreate() {<br />

super.onCreate();<br />

Toast.makeText(this,"Service created ...",<br />

Toast.LENGTH_LONG).show();<br />

paused = false;<br />

Thread initBkgdThread = new Thread(new Runnable() {<br />

public void run() {<br />

play_music();<br />

}<br />

});<br />

initBkgdThread.start();<br />

}<br />

เซอร์วิส<br />

67<br />

@Override<br />

public void onDestroy() {<br />

super.onDestroy();<br />

Toast.makeText(this, "Service destroyed ...",<br />

Toast.LENGTH_LONG).show();<br />

paused = true;<br />

}<br />

}<br />

int[] notes = {R.raw.c5, R.raw.b4, R.raw.a4, R.raw.g4};<br />

int NOTE_DURATION = 400; //millisec<br />

MediaPlayer m_mediaPlayer;<br />

private void play_music() {<br />

for(int ii=0; ii


68 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

ชุดคำสั่งที่ 3.11 AndroidManifest.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

ตัวอย่างของแอคทิวิตี้ที่กำหนดเลย์เอาต์เพื่อสั่งให้เซอร์วิสเริ่มหรือหยุดการทำงาน จะแสดงอยู่<br />

ในชุดคำสั่งที่ 3.12 และไฟล์เลย์เอาต์ที่เกี่ยวข้องกับตัวอย่างนี้จะแสดงอยู่ในชุดคำสั่งที่ 3.13<br />

ชุดคำสั่งที่ 3.12 src/com/cookbook/simple_service/SimpleActivity.java<br />

package com.cookbook.simple_service;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

public class SimpleActivity extends Activity {<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Button startButton = (Button) findViewById(R.id.Button01);<br />

startButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view){<br />

startService(new Intent(SimpleActivity.this,<br />

SimpleService.class));<br />

}<br />

});<br />

Button stopButton = (Button)findViewById(R.id.Button02);


}<br />

}<br />

การสร้างรีซีฟเวอร์เพื่อรับข้อมูลจากอีเวนต์<br />

stopButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View v){<br />

stopService(new Intent(SimpleActivity.this,<br />

SimpleService.class));<br />

}<br />

});<br />

69<br />

ชุดคำสั่งที่ 3.13 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

การสร้างรีซีฟเวอร์เพื่อรับข้อมูลจากอีเวนต์<br />

บรอดคาสต์รีซีฟเวอร์ (Broadcast Receiver) จะคอยรับข้อมูลที่เกี่ยวข้องเพื่อใช้สั่งให้เกิด<br />

เหตุการณ์หรืออีเวนต์ (Event) ต่างๆ ตัวอย่างของอีเวนต์ที่ส่งออกมาจากระบบปฏิบัติการมีดังนี้<br />

m เมื่อกดปุ่มกล้องถ่ายรูป<br />

m เมื่อกำลังไฟของแบตเตอรี่ต่ำ<br />

m เมื่อมีการติดตั้งแอพใหม่<br />

คอมโพเน็นต์ที่เราพัฒนาขึ้นก็สามารถส่งข้อมูลแบบบรอดคาสต์ (Broadcast) ได้เช่นกัน ยก<br />

ตัวอย่างเช่น<br />

m ส่งข้อมูลแบบบรอดคาสต์เมื่อมีการคำนวณเสร็จสิ้น<br />

m ส่งข้อมูลแบบบรอดคาสต์เมื่อเริ่มการทำงานของเธรด<br />

บรอดคาสต์รีซีฟเวอร์ทุกตัวจะสร้างอินสแตนซ์มาจากคลาส BroadcastReceiver<br />

หรือซับคลาสอื่นๆ ที่อยู่ภายใต้คลาสนี้ วงจรการทำงานของบรอดคาสต์รีซีฟเวอร์นั้นไม่ซับซ้อน เมธอด<br />

onReceive() จะถูกเรียกใช้งานเมื่อมีการรับข้อมูลเข้ามา และเมื่อเมธอด onReceive() นี้ทำงาน<br />

เสร็จ อินสแตนซ์ BroadcastReceiver นี้ก็จะหยุดทำงาน


70 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

โดยปกติแล้วบรอดคาสต์รีซีฟเวอร์จะใช้สั่งให้คอมโพเน็นต์เริ่มทำงานหรือส่งข้อความไปยังผู้ใช้<br />

งาน แต่ถ้าเราต้องการให้บรอดคาสต์รีซีฟเวอร์ทำงานร่วมกับคำสั่งที่ต้องใช้ระยะเวลาในการทำงาน<br />

เราจะต้องใช้วิธีสร้างเซอร์วิสเพื่อรองรับการทำงานดังกล่าวในรูปแบบเธรดย่อย เพราะเมื่อบรอดคาสต์<br />

รีซีฟเวอร์ทำงานเสร็จ ตัวมันเองจะถูกยกเลิกจากระบบ<br />

กรรมวิธี: การสั่งให้เซอร์วิสเริ่มทำงานเมื่อมีการกดปุ่มกล้องถ่ายรูป<br />

ชุดคำสั่งนี้จะสาธิตถึงวิธีการสั่งให้เซอร์วิสเริ่มทำงานโดยอาศัยอีเวนต์จากบรอดคาสต์รีซีฟเวอร์<br />

อย่างเช่นการกดปุ่มกล้องถ่ายรูป บรอดคาสต์รีซีฟเวอร์จะรอรับข้อมูลจากเหตุการณ์ที่ก ำหนดไว้ และเริ่ม<br />

การทำงานของเซอร์วิส ซึ่งตัวบรอดคาสต์รีซีฟเวอร์เองจะเริ่มท ำงานจากการสั่งการโดยคอมโพเน็นต์<br />

(เราจึงต้องสร้างแอคทิวิตี้ SimpleActivity ขึ้นมารองรับ)<br />

แอคทิวิตี้ที่แสดงในชุดคำสั่งที่ 3.14 จะแสดงให้เห็นถึงการสร้างบรอดคาสต์รีซีฟเวอร์ รวมถึง<br />

การกำหนดอินเท็นต์และฟิลเตอร์เพื่อทำงานร่วมกับปุ่มกล้องถ่ายรูป ดังนั้นเมื่อบรอดคาสต์รีซีฟเวอร์<br />

เริ่มทำงาน ก็จะมีการกำหนดฟิลเตอร์ที่จะใช้ด้วยเมธอด registerReceiver()<br />

ชุดคำสั่งที่ 3.14 src/com/cookbook/simple_receiver/SimpleActivity.java<br />

package com.cookbook.simple_receiver;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.content.IntentFilter;<br />

import android.os.Bundle;<br />

public class SimpleActivity extends Activity {<br />

SimpleBroadcastReceiver intentReceiver =<br />

new SimpleBroadcastReceiver();<br />

/** Called when the activity is first created. */<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

}<br />

IntentFilter intentFilter =<br />

new IntentFilter(Intent.ACTION_CAMERA_BUTTON);<br />

intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED);<br />

registerReceiver(intentReceiver, intentFilter);<br />

@Override<br />

protected void onDestroy() {<br />

unregisterReceiver(intentReceiver);


}<br />

}<br />

super.onDestroy();<br />

การสร้างรีซีฟเวอร์เพื่อรับข้อมูลจากอีเวนต์<br />

71<br />

ถ้าแอคทิวิตี้ถูกยกเลิก รีซีฟเวอร์ก็จะถูกยกเลิกไปด้วย การทำงานของบรอดคาสต์รีซีฟเวอร์ใน<br />

ชุดคำสั่งที่ 3.15 จะแสดงถึงวงจรการทำงานของเมธอด onReceive() ที่ทำหน้าที่คอยรับบรอดคาสต์<br />

อีเวนต์ต่างๆ ถ้ามีสร้างบรอดคาสต์อีเวนต์ที่ตรงกับอีเวนต์ที่กำหนดไว้ (ในที่นี้คืออีเวนต์ ACTION_CAM-<br />

ERA_BUTTON) เซอร์วิสก็จะเริ่มทำงาน<br />

ชุดคำสั่งที่ 3.15 src/com/cookbook/simple_receiver/SimpleBroadcastReceiver .java<br />

package com.cookbook.simple_receiver;<br />

import android.content.BroadcastReceiver;<br />

import android.content.Context;<br />

import android.content.Intent;<br />

public class SimpleBroadcastReceiver extends BroadcastReceiver {<br />

@Override<br />

public void onReceive(Context rcvContext, Intent rcvIntent) {<br />

String action = rcvIntent.getAction();<br />

if (action.equals(Intent.ACTION_CAMERA_BUTTON)) {<br />

rcvContext.startService(new Intent(rcvContext,<br />

SimpleService2.class));<br />

}<br />

}<br />

}<br />

เซอร์วิสที่เริ่มทำงานใน SimpleBroadcastReceiver จากชุดคำสั่งที่ 3.15 ได้แสดงอยู่ในชุด<br />

คำสั่งที่ 3.16 ซึ่งเราใช้ Toast ในการสั่งให้เซอร์วิสเริ่มหรือหยุดการทำงาน<br />

ชุดคำสั่งที่ 3.16 src/com/cookbook/simple_receiver/SimpleService2.java<br />

package com.cookbook.simple_receiver;<br />

import android.app.Service;<br />

import android.content.Intent;<br />

import android.os.IBinder;<br />

import android.widget.Toast;<br />

public class SimpleService2 extends Service {<br />

@Override<br />

public IBinder onBind(Intent arg0) {<br />

return null;<br />

}


72 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

@Override<br />

public void onCreate() {<br />

super.onCreate();<br />

Toast.makeText(this,"Service created ...",<br />

Toast.LENGTH_LONG).show();<br />

}<br />

}<br />

@Override<br />

public void onDestroy() {<br />

super.onDestroy();<br />

Toast.makeText(this, “Service destroyed ...",<br />

Toast.LENGTH_LONG).show();<br />

}<br />

แอพวิดเจ็ต (App Widget)<br />

แอพวิดเจ็ต (App Widget) จะมีลักษณะเป็นแอพย่อยตัวเล็กๆ ที่ทำงานร่วมกับแอพหลัก<br />

แอพวิดเจ็ตจะทำงานกับบรอดคาสต์รีซีฟเวอร์เพื่อคอยอัพเดตข้อมูลต่างๆ ในบางครั้งเราจะเรียก<br />

แอพวิดเจ็ตแบบสั้นๆ ว่า “วิดเจ็ต” มันสามารถทำงานแบบฝังตัวร่วมกับแอพอื่นๆ ได้ เช่นกับหน้า<br />

Home เป็นต้น วิธีใช้แอพวิดเจ็ตจะเริ่มต้นโดยการแตะค้างตรงตำแหน่งที่ว่างบนจอภาพ จากนั้นบนจอ<br />

ก็จะแสดงเมนูของรายการวิดเจ็ตที่เราสามารถเลือกใช้งานได้ พอเลือกแล้ววิดเจ็ตก็จะแสดงอยู่บน<br />

หน้าจอเพื่อทำงานตามที่กำหนดไว้ ในขณะเดียวกันถ้าเราต้องการลบวิดเจ็ตออกจากหน้าจอ ก็ให้คลิก<br />

บนวิดเจ็ตและลากมันไว้ที่ไอคอนรูปถังขยะ การสร้างแอพวิดเจ็ตนั้นมีรายละเอียดดังนี้<br />

m กำหนดรูปแบบการแสดงผลของวิดเจ็ตโดยระบุค่าเลย์เอาต์ลงไปในไฟล์ XML<br />

ซึ่งจะประกอบด้วยไฟล์รีซอร์สต่างๆ, ข้อความ, พื้นหลัง และค่าพารามิเตอร์อื่นๆ<br />

m กำหนดแอพวิดเจ็ตโพรวายเดอร์ (Provider) ที่ใช้ในการรับบรอดคาสต์อีเวนต์และติดต่อ<br />

กับวิดเจ็ตเพื่อทำการอัพเดตข้อมูล<br />

m กำหนดรายละเอียดของแอพวิดเจ็ต เช่น ขนาดและระยะเวลาความถี่ของการอัพเดต<br />

ข้อมูล ในหน้า Home นั้นจะมีขนาด 4x4 เซลล์ ส่วนวิดเจ็ตโดยทั่วไปจะมีขนาดใหญ่กว่า<br />

1x1 เซลล์<br />

m สามารถสร้างแอคทิวิตี้เพื่อเก็บค่าคอนฟิก (Configuration) ต่างๆ ของแอพวิดเจ็ตได้<br />

เช่นกัน ซึ่งแอคทิวิตี้นี้จะเริ่มทำงานเมื่อมีการเพิ่มวิดเจ็ตลงบนจอภาพ<br />

กรรมวิธี: การสร้างแอพวิดเจ็ต<br />

ในส่วนนี้จะสาธิตการสร้างแอพวิดเจ็ตอย่างง่ายเพื่อทำหน้าที่แสดงข้อความบนหน้า Home<br />

ซึ่งข้อความที่แสดงนั้นจะมีการอัพเดตทุกๆ 1 วินาที แต่ในระบบปฏิบัติการแอนดรอยด์จะบังคับให้<br />

กำหนดระยะเวลาอัพเดตต่ำสุดได้เพียง 30 นาทีเท่านั้น เพื่อป้องการทำงานของแอพวิดเจ็ตบางตัวที่มี<br />

ผลทำให้กินแบตเตอรี่มากกว่าปกติ ในชุดคำสั่งที่ 3.17 จะแสดงการสร้าง AppWidgetProvider<br />

ซึ่งเป็นซับคลาสของ BroadcastReceiver เมธอดหลักที่เราจะโอเวอร์ไรด์คือ onUpdate()<br />

ซึ่งเมธอดนี้จะถูกเรียกใช้งานเมื่อครบรอบเวลาที่จะต้องอัพเดตวิดเจ็ต


แอพวิดเจ็ต (App Widget)<br />

ชุดคำสั่งที่ 3.17 src/com/cookbook/widget_example/SimpleWidgetProvider.java<br />

73<br />

package com.cookbook.simple_widget;<br />

import android.appwidget.AppWidgetManager;<br />

import android.appwidget.AppWidgetProvider;<br />

import android.content.Context;<br />

import android.widget.RemoteViews;<br />

public class SimpleWidgetProvider extends AppWidgetProvider {<br />

final static int <strong>APP</strong>WIDGET = 1001;<br />

@Override<br />

public void onUpdate(Context context,<br />

AppWidgetManager appWidgetManager, int[] appWidgetIds) {<br />

super.onUpdate(context, appWidgetManager, appWidgetIds);<br />

// Loop through all widgets to display an update<br />

final int N = appWidgetIds.length;<br />

for (int i=0; i


74 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

ชุดคำสั่งที่ 3.18 จะแสดงข้อมูลภายในไฟล์ XML ที่ประกาศรายละเอียดของวิดเจ็ต โดยจะมี<br />

ข้อมูลขนาดของวิดเจ็ตที่แสดงอยู่บนหน้าจอ Home และค่าความถี่ของช่วงเวลาที่จะทำการอัพเดต<br />

ข้อมูลโดยใช้หน่วยเป็นมิลลิวินาที (ค่าต่ำสุดของระบบเป็น 30 นาที)<br />

ชุดคำสั่ง 3.18 src/res/xml/widget_info.xml<br />

<br />

<br />

<br />

ชุดคำสั่งที่ 3.19 จะแสดงข้อมูลภายในไฟล์ XML เกี่ยวกับเลย์เอาต์ของวิดเจ็ต<br />

ชุดคำสั่ง 3.19 src/res/layout/widget_layout.xml<br />

<br />

<br />

การแจ้งเตือน (Alert)<br />

การแจ้งเตือน (Alert) เป็นข้อมูลประเภทข้อความที่ทำงานอยู่นอกแอพ สามารถแสดงผลแบบ<br />

ซ้อนอยู่บนหน้าต่างของแอพได้ไม่ต่างกับ Toast หรือ AlertDialog และการแจ้งเตือนนี้สามารถกำหนด<br />

ให้แสดงผลอยู่บนเมนูบาร์ตรงด้านบนของจอได้ โดย Toast จะแสดงข้อความแจ้งเตือนบนจอด้วย<br />

ข้อความหนึ่งบรรทัด ซึ่งการแสดงผลนี้เราไม่จำเป็นต้องสร้างเลย์เอาต์ให้แก่ข้อความแจ้งเตือนนี้<br />

และจากคุณสมบัตินี้เอง ทำให้เราสามารถเอาการทำงานของข้อความแจ้งเตือนมาใช้ดีบั๊กหาข้อผิด<br />

พลาดภายในแอพได้ คล้ายกับการใช้คำสั่ง printf ในภาษา C<br />

กรรมวิธี: การแสดงข้อความแจ้งเตือนบนจอภาพ<br />

ในบทก่อนหน้านี้เราได้ใช้คำสั่ง Toast มาแล้ว โดยใช้รูปแบบของคำสั่งดังนี้<br />

Toast.makeText(this, “text”, Toast.LENGTH_SHORT).show();<br />

จากคำสั่งข้างต้น เราสามารถเขียนให้อยู่ในลักษณะหลายๆ บรรทัดได้ดังนี้<br />

Toast tst = Toast.makeText(this, “text”, Toast.LENGTH_SHORT);<br />

tst.show();


การเขียนคำสั่งในลักษณะนี้จะมีประโยชน์ในกรณีที่เราต้องการจะแสดงข้อความหลายครั้ง<br />

ซึ่งเราจะใช้คำสั่งของอินสแตนซ์ในบรรทัดแรกมาใช้ซ้ำ<br />

ในคำสั่ง Toast จะมีคำสั่งอีก 2 บรรทัดที่ใช้ในการกำหนดตำแหน่งของข้อความที่จะแสดง หรือ<br />

ตำแหน่งที่จะวางรูปภาพ ในการกำหนดตำแหน่งของข้อความหรือกำหนดให้แสดงที่กลางจอภาพ เราจะ<br />

ใช้คำสั่ง setGravity ก่อนที่จะเรียกใช้เมธอด show() ดังนี้<br />

tst.setGravity(Gravity.CENTER, tst.getXOffset() / 2,<br />

tst.getYOffset() / 2);<br />

การเพิ่มรูปภาพลงใน Toast จะใช้คำสั่งดังนี้<br />

Toast tst = Toast.makeText(this, “text”, Toast.LENGTH_LONG);<br />

ImageView view = new ImageView(this);<br />

view.setImageResource(R.drawable.my_figure);<br />

tst.setView(view);<br />

tst.show();<br />

กรรมวิธี: การใช้งานข้อความแจ้งเตือน<br />

เราจะทำการแสดงข้อความแจ้งเตือน และแสดงปุ่ม 3 ปุ่มให้ผู้ใช้เลือก โดยใช้การทำงานของ<br />

คลาส AlertDialog โดยตัวอย่างของข้อความที่จะแสดงก็เช่น<br />

m “คะแนนของคุณคือ 80/100 คุณจะลองเล่นระดับนี้ใหม่ หรือจะไปที่ระดับถัดไป หรือจะ<br />

กลับสู่เมนูหลัก”<br />

m “พบความเสียหายในไฟล์รูปภาพ โปรดเลือกไฟล์อื่นๆ หรือออกจากแอพ”<br />

ในส่วนนี้จะแสดงถึงการสร้างตัวเลือกแบบหลายตัวเลือกเพื่อให้ผู้ใช้ได้เลือก ตัวอย่างของ<br />

คำสั่งจะแสดงอยู่ในชุดคำสั่งที่ 3.20<br />

AlertDialog จะเริ่มทำงานด้วยเมธอด create() และกำหนดข้อความที่จะแสดงด้วยเมธอด<br />

setMessage() ส่วนปุ่มทั้ง 3 ปุ่มที่จะแสดงให้ผู้ใช้เลือกนั้น จะใช้เมธอด setButton() และคำสั่ง<br />

สุดท้ายที่จะใช้ในการแสดงข้อความบนหน้าจอก็คือเมธอด show() สำหรับคำสั่งที่อยู่ใน onClick()<br />

นั้นจะเป็นคำสั่งแบบ Callback ซึ่งในกรณีนี้เราจะกล่าวถึงแค่การสร้างปุ่มแบบตัวเลือกเท่านั้น<br />

ชุดคำสั่ง 3.20 ตัวอย่างการใช้งาน AlertDialog<br />

การแจ้งเตือน (Alert)<br />

75<br />

AlertDialog dialog = new AlertDialog.Builder(this).create();<br />

dialog.setMessage("Your final score: " + mScore + "/" + PERFECT_SCORE);<br />

dialog.setButton(DialogInterface.BUTTON_POSITIVE, "Try this level again",<br />

new DialogInterface.OnClickListener() {<br />

public void onClick(DialogInterface dialog, int which) {<br />

mScore = 0;<br />

start_level();<br />

}<br />

});<br />

dialog.setButton(DialogInterface.BUTTON_NEGATIVE, "Advance to next level",<br />

new DialogInterface.OnClickListener() {


76 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

public void onClick(DialogInterface dialog, int which) {<br />

mLevel++;<br />

start_level();<br />

}<br />

});<br />

dialog.setButton(DialogInterface.BUTTON_NEUTRAL, "Back to the main menu",<br />

new DialogInterface.OnClickListener() {<br />

public void onClick(DialogInterface dialog, int which) {<br />

mLevel = 0;<br />

finish();<br />

}<br />

});<br />

dialog.show();<br />

จากชุดคำสั่งข้างบนจะแสดงข้อความแจ้งเตือนดังรูปที่ 3.3 จะสังเกตเห็นว่าปุ่มทั้ง 3 แสดง<br />

เรียงกันตามคำสั่งที่เขียนไว้คือ BUTTON_POSITIVE , BUTTON_NEUTRAL , BUTTON_NEGATIVE<br />

ซึ่งถ้าต้องการใช้ปุ่มเพียงแค่ 1 หรือ 2 ปุ่ม เราก็สามารถเขียนชุดคำสั่งตามจำนวนที่ต้องการได้<br />

รูปที่ 3.3 ตัวอย่างของกล่องข้อความแจ้งเตือน<br />

กรรมวิธี: การแสดงข้อความบนสเตตัสบาร์<br />

สเตตัสบาร์ (Status Bar) อยู่ตรงส่วนบนสุดของจอเพื่อแสดงข้อความแจ้งเตือนต่างๆ ที่รอให้<br />

ผู้ใช้อ่านในภายหลัง โดยทั่วไปแล้วการแสดงข้อมูลในส่วนนี้ มักเป็นข้อความที่กระชับและเข้าใจง่าย<br />

เพื่อให้แอพทำงานได้อย่างมีประสิทธิภาพ<br />

ขั้นตอนการสร้างข้อความบนสเตตัสบาร์มีดังนี้<br />

1. สร้างข้อความแจ้งเตือน และกำหนดให้แสดงผลบนสเตตัสบาร์<br />

String ns = Context.NOTIFICATION_SERVICE;<br />

mNManager = (NotificationManager) getSystemService(ns);<br />

final Notification msg = new Notification(R.drawable.icon,<br />

“New event of importance”,<br />

System.currentTimeMillis());


2. กำหนดรูปแบบที่จะแสดงเมื่อขยายสเตตัสบาร์เพื่อแสดงรายละเอียดและการทำงานต่างๆ<br />

เวลาที่มีการกดปุ่ม (การทำงานเหล่านี้จะถูกประกาศโดยคลาส PendingIntent)<br />

Context context = getApplicationContext();<br />

CharSequence contentTitle = “ShowNotification Example”;<br />

CharSequence contentText = “Browse Android Cookbook Site”;<br />

Intent msgIntent = new Intent(Intent.ACTION_VIEW,<br />

Uri.parse(“http://www.pearson.com”));<br />

PendingIntent intent =<br />

PendingIntent.getActivity(ShowNotification.this,<br />

0, msgIntent,<br />

Intent.FLAG_ACTIVITY_NEW_TASK);<br />

3. เพิ่มรายละเอียดประกอบการแจ้งเตือนต่างๆ เช่น หลอดไฟ LED กะพริบ, การเล่นเสียง<br />

ต่างๆ หรือการปิดข้อความแจ้งเตือนโดยอัตโนมัติเมื่อข้อความนั้นถูกอ่านแล้ว<br />

msg.defaults |= Notification.DEFAULT_SOUND;<br />

msg.flags |= Notification.FLAG_AUTO_CANCEL;<br />

4. กำหนดค่าของอีเวนต์ข้อความแจ้งเตือนให้แก่ระบบปฏิบัติการ<br />

msg.setLatestEventInfo(context,<br />

contentTitle, contentText, intent);<br />

5. เมื่อมีเหตุการณ์ตรงกับอีเวนต์ที่ระบุไว้ ก็จะแสดงข้อความแจ้งเตือน ซึ่งข้อความเหล่านี้จะ<br />

มีเลขประจำข้อความ<br />

mNManager.notify(NOTIFY_ID, msg);<br />

6. เมื่อแสดงข้อความแจ้งเตือนเรียบร้อยแล้ว ให้ลบข้อความเหล่านั้นโดยอ้างอิงจากเลข<br />

ประจำข้อความ<br />

ถ้าข้อความที่จะใช้แจ้งเตือนมีการเปลี่ยนแปลง เราก็จะใช้วิธีอัพเดตข้อความเดิม แทนที่จะส่ง<br />

ข้อความแจ้งเตือนอันใหม่ การอัพเดตข้อความนั้นเราจะทำตามวิธีในขั้นตอนที่ 2 และเรียกใช้<br />

setLatestEventInfo อีกครั้ง ตัวอย่างของการแสดงข้อความแจ้งเตือนนั้นแสดงอยู่ในชุดค ำสั่งที่ 3.21<br />

ชุดคำสั่ง 3.21 src/com/cookbook/show_notification/ShowNotification.java<br />

package com.cookbook.show_notification;<br />

import android.app.Activity;<br />

import android.app.Notification;<br />

import android.app.NotificationManager;<br />

import android.app.PendingIntent;<br />

import android.content.Context;<br />

import android.content.Intent;<br />

import android.net.Uri;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

การแจ้งเตือน (Alert)<br />

77


78 บทที่ 3 เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน<br />

public class ShowNotification extends Activity {<br />

private NotificationManager mNManager;<br />

private static final int NOTIFY_ID=1100;<br />

/** Called when the activity is first created. */<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

String ns = Context.NOTIFICATION_SERVICE;<br />

mNManager = (NotificationManager) getSystemService(ns);<br />

final Notification msg = new Notification(R.drawable.icon,<br />

"New event of importance",<br />

System.currentTimeMillis());<br />

Button start = (Button)findViewById(R.id.start);<br />

Button cancel = (Button)findViewById(R.id.cancel);<br />

start.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

Context context = getApplicationContext();<br />

CharSequence contentTitle = "ShowNotification Example";<br />

CharSequence contentText = "Browse Android Cookbook Site";<br />

Intent msgIntent = new Intent(Intent.ACTION_VIEW,<br />

Uri.parse("http://www.pearson.com"));<br />

PendingIntent intent =<br />

PendingIntent.getActivity(ShowNotification.this,<br />

0, msgIntent,<br />

Intent.FLAG_ACTIVITY_NEW_TASK);<br />

msg.defaults |= Notification.DEFAULT_SOUND;<br />

msg.flags |= Notification.FLAG_AUTO_CANCEL;<br />

});<br />

}<br />

msg.setLatestEventInfo(context,<br />

contentTitle, contentText, intent);<br />

mNManager.notify(NOTIFY_ID, msg);<br />

}<br />

}<br />

cancel.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

mNManager.cancel(NOTIFY_ID);<br />

}<br />

});


79<br />

บทที่ 4<br />

ส่วนการติดต่อกับผู้ใช้งาน<br />

(User Interface)<br />

ในระบบปฏิบัติการแอนดรอยด์นั้น ส่วนการติดต่อกับผู้ใช้งาน (UI – User Interface)<br />

จะประกอบไปด้วย ส่วนของจอ, การสัมผัสจอ และการกดปุ่มต่างๆ ทาง Google ได้พัฒนาเฟรมเวิร์ค<br />

เพื่อสนับสนุนการทำงานเหล่านี้เอาไว้ ทำให้สามารถรองรับกับการทำงานร่วมกันในอุปกรณ์แอนดรอยด์<br />

หลายๆ แบบได้ ในบทนี้เราจะมาทำความรู้จักกับเฟรมเวิร์คที่นำมาใช้ในการสร้างกราฟิกเลย์เอาต์<br />

และการเปลี่ยนแปลงบนจอภาพตามเหตุการณ์ต่างๆ แล้วในบทที่ 5 เราค่อยมาพูดถึงเหตุการณ์ต่างๆ<br />

ที่เกี่ยวพันกับการกดปุ่มและเจสเจอร์ (Gesture) กัน<br />

โครงสร้างของรีซอร์สในไดเร็กทอรี และค่าแอททริบิวต์<br />

ที่เกี่ยวข้อง<br />

การแสดงผลบนจอภาพโดยการกำหนดเลย์เอาต์นั้น เราจะใช้ไฟล์รีซอร์สที่เราสร้างขึ้น ซึ่งไฟล์<br />

เหล่านี้ได้พูดถึงแล้วในบทที่ 2 “การพัฒนาแอพเบื้องต้น: แอคทิวิตี้ และ อินเท็นต์” โดยไฟล์จะถูกจัด<br />

เก็บอยู่ในโปรเจ็กต์แอนดรอยด์ ด้านล่างนี้คือการสรุปแบบคร่าวๆ ถึงโครงสร้างของรีซอร์สใน<br />

ไดเร็กทอรี<br />

m res/anim/ -- จัดเก็บไฟล์ประเภทภาพเคลื่อนไหวในรูปแบบเฟรม<br />

m res/drawable/ -- จัดเก็บไฟล์ประเภทรูปภาพ ซึ่งสามารถแก้ไขและปรับแต่งให้มีขนาดที่<br />

เหมาะสมในระหว่างการคอมไพล์แอพได้<br />

m res/layout/ -- จัดเก็บไฟล์ประเภท XML (Extensible Markup Language) ที่ใช้ในการ<br />

กำหนดเลย์เอาต์<br />

m res/values/ -- จัดเก็บไฟล์ประเภท XML ที่ประกาศรายละเอียดของรีซอร์ส เช่น<br />

arrays.xml, colors.xml, dimens.xml, strings.xml และ styles.xml<br />

m res/xml/ -- จัดเก็บไฟล์ประเภท XML อื่นๆ ที่ไม่เข้าพวกกับกลุ่มข้างต้น<br />

m res/raw/ -- จัดเก็บไฟล์รีซอร์สอื่นๆ ที่ไม่เข้าพวกกับกลุ่มข้างต้น เช่น ไฟล์รูปภาพที่ไม่<br />

สามารถแก้ไขได้ เป็นต้น<br />

ออบเจ็กต์ UI แต่ละตัวจะมีค่าแอททริบิวต์ที่สามารถกำหนดได้จำนวน 3 ค่า ซึ่งค่าเหล่านี้จะใช้<br />

ในการปรับแต่ง UI ได้แก่ ค่าของขนาดออบเจ็กต์, ข้อความที่แสดงในออบเจ็กต์ และสีของออบเจ็กต์<br />

โดยรูปแบบของค่าที่จะใช้กำหนดให้แก่แอททริบิวต์เหล่านี้จะแสดงไว้ในตารางที่ 4.1 เวลากำหนดขนาด<br />

ของออบเจ็กต์นั้น ควรใช้หน่วยเป็น dp หรือ sp เพื่อให้สามารถรองรับการทำงานบนอุปกรณ์ได้หลาย<br />

แบบ


80 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

ตารางที่ 4.1 รูปแบบของค่าที่จะใช้กำหนดให้แก่แอททริบิวต์ของออบเจ็กต์ UI<br />

แอททริบิวต์<br />

รูปแบบของค่า<br />

ขนาดของออบเจ็กต์ ค่าตัวเลข ที่ตามด้วยหน่วยดังนี้<br />

px – ค่าพิกเซลจริงของจอภาพ<br />

dp (หรือ dip)— ค่าพิกเซลอ้างอิงตามจอขนาด 160dpi<br />

sp – ค่าพิกเซลอ้างอิงตามขนาดของฟอนต์ที่ผู้ใช้เลือกไว้<br />

in – ค่าความยาวเป็นนิ้วที่วัดตามขนาดจริงของจอ<br />

mm – ค่าความยาวเป็นมิลลิเมตรที่วัดตามขนาดจริงของจอ<br />

pt – ค่าความยาว 1/72 นิ้วตามขนาดจริงของจอ<br />

ข้อความ ข้อความใดๆ ก็ตาม สามารถมีความยาวไปจนกว่าจะเจอเครื่องหมาย ‘ แต่ถ้าจะใช้เครื่องหมายนี้ก็<br />

ให้พิมพ์นำาหน้าด้วยเครื่องหมาย \ เช่น Don\’t worry<br />

ข้อความที่อยู่ในเครื่องหมายคำาพูด เช่น “Don’t worry”<br />

ข้อความที่มีการจัดรูปแบบ เช่น Population: %1$d<br />

ใส่แท็กของ HTML ได้ เช่น , , <br />

ใส่ตัวอักษรพิเศษได้ เช่น &#169;<br />

สี<br />

สามารถกำาหนดค่าสีแบบ 12 บิต #rgb, แบบ 16 บิต #argb, แบบ 24 บิต #rrggbb หรือแบบ<br />

32 บิต #aarrggbb ได้ รวมทั้งยังสามารถกำาหนดสีด้วยการระบุเป็นค่าคงที่ของระบบแบบที่ใช้ใน<br />

ภาษาจาวาได้ด้วย เช่น Color.CYAN<br />

การกำหนดรูปแบบและมุมมองของแอพให้เป็นไปในทิศทางเดียวกันนั้น เราจะใช้คุณสมบัติของ<br />

ไฟล์รีซอร์สแบบโกลบอล (Global) มาช่วยในการกำหนดค่าของแอททริบิวต์ ซึ่งจะช่วยให้ง่ายในการนำ<br />

ค่าเหล่านี้มาใช้ในภายหลัง โดยเราจะเก็บค่านี้ในรูปแบบของไฟล์ XML อย่างเช่น<br />

m ขนาดของออบเจ็กต์ จะประกาศในไฟล์ XML res/values/dimens.xml ตัวอย่างเช่น<br />

* การประกาศค่าใน XML เช่น 48sp<br />

* การอ้างอิงด้วย XML เช่น @dimen/large<br />

* การอ้างอิงด้วยจาวา เช่น getResources().getDimension(R.dimen.large)<br />

m ข้อความและลาเบล จะประกาศในไฟล์ XML res/values/strings.xml ตัวอย่างเช่น<br />

* การประกาศค่าใน XML เช่น I\’m here<br />

* การอ้างอิงด้วย XML เช่น @string/start_pt<br />

* การอ้างอิงด้วยจาวา เช่น getBaseContext().getString(R.string.start_pt)<br />

m สีของออบเจ็กต์ จะประกาศในไฟล์ XML res/values/colors.xml ตัวอย่างเช่น<br />

* การประกาศค่าใน XML เช่น #f00<br />

* การอ้างอิงด้วย XML เช่น @color/red<br />

* การอ้างอิงด้วยจาวา เช่น getResources().getColor(R.color.red)


กรรมวิธี: การระบุรีซอร์สเพิ่มเติม<br />

ค่ารีซอร์สที่เราได้กำหนดไว้ในหัวข้อก่อนหน้านี้เป็นการกำหนดค่าคอนฟิกแบบทั่วๆ ไปที่มีใช้ใน<br />

แอพของแอนดรอยด์ ในบางครั้งผู้พัฒนาแอพอาจต้องการระบุค่าต่างๆ เพิ่มเติมขึ้นเองเพื่อให้รองรับ<br />

กับแอพที่พัฒนาขึ้น อย่างเช่นทำให้แอพสามารถแสดงผลได้หลายภาษา เป็นต้น<br />

วิธีทำให้แอพรองรับการแสดงผลได้หลายภาษานั้น เราต้องแปลข้อความที่จะแสดงผลนั้นไปเป็น<br />

ภาษาต่างๆ ตามที่ต้องการก่อน และนำไปกำหนดไว้ในไดเร็กทอรี values ยกตัวอย่างเช่น ภาษาอังกฤษ<br />

(อเมริกัน), ภาษาอังกฤษ (อังกฤษ), ภาษาฝรั่งเศส, ภาษาจีน, ภาษาจีน (ไต้หวัน) และภาษาเยอรมัน<br />

ก็ให้กำหนดดังนี้<br />

res/values-en-rUS/strings.xml<br />

res/values-en-rGB/strings.xml<br />

res/values-fr/strings.xml<br />

res/values-zh-rCN/strings.xml<br />

res/values-zh-rTW/strings.xml<br />

res/values-de/strings.xml<br />

ข้อความทั้งหมดที่จะใช้ในแอพนั้น ไม่จำเป็นต้องเก็บลงในไฟล์เหล่านี้ทั้งหมด เพราะถ้าระบบพบ<br />

ว่าข้อความที่ต้องการแสดงผลในภาษาที่เลือกไว้ไม่ได้ถูกกำหนดไว้ ระบบจะอ่านค่าจากไฟล์หลักของ<br />

ระบบแทน คือ res/values/strings.xml โดยไฟล์นี้จะเก็บข้อความทั้งหมดที่มีการใช้ในแอพ และถ้ามี<br />

รูปภาพที่มีข้อความแบบหลายภาษา เราก็สามารถกำหนดโครงสร้างของไดเร็กทอรีที่เก็บข้อมูลดังกล่าว<br />

ได้เช่นกัน อย่างเช่น res/drawables-zh-hdpi/<br />

สำหรับการกำหนดให้แอพรองรับการแสดงผลบนจอที่มีความละเอียดต่างกันนั้น รูปภาพประเภทที่<br />

ปรับเปลี่ยนขนาดได้ (Drawable) จะถูกนำมาใช้ในการแสดงผลด้วยการกำหนดให้โครงสร้างของ<br />

ไดเร็กทอรีระบุถึงความละเอียดระดับต่างๆ เช่นว่าเราสามารถกำหนดการแสดงรูปภาพด้วยความละเอียด<br />

ต่างๆ กันได้ดังนี้<br />

res/drawable-ldpi/<br />

res/drawable-mdpi/<br />

res/drawable-hdpi/<br />

โครงสร้างของรีซอร์สในไดเร็กทอรี และค่าแอททริบิวต์ที่เกี่ยวข้อง<br />

res/drawable-nodpi/<br />

ระดับความละเอียดต่ำ กลาง และสูงของจอภาพนั้น เรากำหนดค่าเป็น 120dpi, 160 dpi และ<br />

240 dpi ตามลำดับ โดยค่าทั้งหมดนี้เราไม่จำเป็นต้องใช้เรียกงานทั้งหมด เพราะในระหว่างที่แอพเริ่ม<br />

ทำงานนั้นระบบปฏิบัติการแอนดรอยด์จะเลือกใช้ค่าที่เหมาะสมกับฮาร์ดแวร์นั้นมากที่สุด ในไดเร็กทอรี<br />

ที่ระบุค่าเป็น nodpi จะใช้กับรูปภาพประเภทบิตแม็พในกรณีที่ไม่ต้องการให้รูปภาพนั้นถูกปรับขนาด<br />

ดังนั้นถ้ามีการระบุภาษาที่จะใช้และค่าความละเอียดของจอที่จะแสดงผล เราก็สามารถสร้างไดเร็กทอรี<br />

ที่ใช้ทำงานร่วมกับค่าทั้ง 2 นั้นได้ เช่น drawable-en-rUS-mdpi/<br />

เราได้พูดถึงขนาดของจอภาพที่ใช้แสดงผลบนอุปกรณ์แอนดรอยด์ไปแล้วในบทที่ 1 “ก้าวแรก<br />

กับแอนดรอยด์” การจัดโครงสร้างของไดเร็กทอรีรีซอร์สให้มีลักษณะตามที่บอกไปข้างต้นจะช่วยให้<br />

แอพสามารถแสดงผลบนจอภาพที่แตกต่างกันได้อย่างหลากหลาย ค่าอื่นๆ ที่เราสามารถนำมากำหนด<br />

รูปแบบการแสดงผลเพิ่มเติมมีดังนี้<br />

m การแสดงผลจอภาพแบบแนวตั้งและแนวนอน: -port และ -land<br />

m จอภาพแบบปกติ (QVGA, HVGA, VGA) และจอภาพแบบกว้าง (WQVGA, FWVGA,<br />

WVGA): -notlong และ -long<br />

81


82 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

m จอภาพขนาดเล็ก (น้อยกว่า 3 นิ้ว), จอภาพขนาดปกติ (3-4.5 นิ้ว) และจอภาพขนาดใหญ่<br />

(มากกว่า 4.5 นิ้ว): -small, -normal และ –large<br />

ถ้าระบบพบว่าไม่มีการกำหนดลักษณะการแสดงจอภาพหรือขนาดจอภาพไว้ ระบบปฏิบัติการ<br />

แอนดรอยด์จะทำการปรับขนาดของ UI ให้เหมาะสมกับจอภาพที่ใช้งานอยู่ หลังจากที่เรากำหนดไฟล์<br />

เลย์เอาต์ที่จะแสดงผลบนจอต่างๆ แล้ว เราจะต้องเพิ่มอีลีเมนต์ลงไปในไฟล์ Manifest ด้วย<br />

เพื่อประกาศถึงรูปแบบของจอภาพที่แอพสามารถทำงานด้วยได้<br />

<br />

ถ้าค่าของ android:minSdkVersion หรือ android:targetSdkVersion มีค่าเท่ากับ 3 (ใช้ระบบ<br />

ปฏิบัติการแอนดรอยด์ 1.5) เราก็จะกำหนดค่า true ให้แก่ android:normalScreens เพื่อให้แสดงผล<br />

บนจอของ HTC รุ่น G1 ได้เหมาะสม ดังนั้นเราควรประกาศค่าให้แก่อีลีเมนต์ supports-screens<br />

เพื่อให้ระบบปฏิบัติการรู้ว่าแอพนั้นรองรับจอขนาดใดบ้าง<br />

วิวและกลุ่มของวิว<br />

พื้นฐานการทำงานร่วมกับเลย์เอาต์บนจอภาพนั้นคือการใช้งานวิว (View) วิวแต่ละวิวจะมี<br />

ลักษณะการทำงานแบบออบเจ็กต์ที่ใช้แทนพื้นที่รูปสี่เหลี่ยมบนจอภาพและเหตุการณ์หรืออีเวนต์ที่เกิด<br />

ขึ้นในพื้นที่นั้น คลาส View สามารถเรียกใช้วิดเจ็ตให้ทำงานได้ ตัวอย่างเช่น การนำปุ่มหรือเช็คบ็อกซ์<br />

มาทำงานร่วมกัน<br />

กลุ่มของวิว (ViewGroup) เป็นออบเจ็กต์ที่มีลักษณะเป็นคอนเทนเนอร์ (Container) ที่สามารถ<br />

เก็บวิวได้หลายตัว ยกตัวอย่างเช่น ViewGroup สามารถเก็บ View ที่แทนพื้นที่แบบแนวตั้งและ<br />

แนวนอนได้ ดังรูปที่ 4.1<br />

รูปที่ 4.1 ตัวอย่างของวิวที่แสดงถึงกลุ่มของวิวและวิดเจ็ต


เลย์เอาต์เป็นการประกาศถึงอีลีเมนต์ของ UI, ตำแหน่งที่วาง และแอ็กชั่นต่างๆ ของอีลีเมนต์<br />

นั้น เลย์เอาต์สามารถประกาศได้ทั้งในไฟล์ XML และไฟล์จาวา แต่ส่วนใหญ่มักใช้ประกาศเลย์เอาต์ใน<br />

ไฟล์ XML และประกาศการทำงานต่างๆ ของแอพไว้ในไฟล์จาวา รูปแบบนี้จะช่วยให้การพัฒนาแอพมี<br />

ความง่ายและยืดหยุ่นมากขึ้น โดยเราได้กำหนดส่วนของจอภาพไว้ในไฟล์ XML และกำหนดคำสั่งการ<br />

ทำงานด้วยภาษาจาวา<br />

ประโยชน์อีกอย่างหนึ่งของการแยกเลย์เอาต์ออกจากจาวาแอคทิวิตี้มาเก็บไว้ในไฟล์ XML ก็คือ<br />

แอพสามารถทำงานแยกตามชนิดของอุปกรณ์ (โทรศัพท์หรือแท็บเล็ต), จอแสดงผล และภาษาที่<br />

ต้องการใช้งานได้ (ภาษาจีนหรือภาษาอังกฤษ) โดยเราสามารถแก้ไขค่าต่างๆ ภายในไฟล์ XML ได้โดย<br />

ไม่ส่งผลกระทบต่อการทำงาน<br />

กรรมวิธี: การสร้างเลย์เอาต์ในโปรแกรม Eclipse<br />

วิธีที่ง่ายและเร็วที่สุดในการสร้างเลย์เอาต์ก็คือการใช้เครื่องมือกราฟิกเลย์เอาต์ (Layout<br />

Editor) ใน Eclipse โดยสร้างแอคทิวิตี้ขึ้นใหม่และเปิดไฟล์เลย์เอาต์ XML ในที่นี้คือไฟล์ mail.xml<br />

หลังจากนั้นให้กดที่แท็บชื่อ Layout หน้าจอก็จะแสดงเลย์เอาต์ในรูปแบบกราฟิก ให้เลือกที่จอดำ<br />

และลบทุกอย่างออกให้หมดเพื่อสร้างเลย์เอาต์ขึ้นใหม่ตามขั้นตอนดังนี้<br />

1. คลิกและลากเลย์เอาต์จากตัวเลือกเลย์เอาต์มาวางลงบนพื้นที่ของจอภาพ อย่างเช่น<br />

เลือก TableLayout ซึ่งจะวาง Views และ ViewGroups ในแนวตั้ง<br />

2. คลิกและลากเลย์เอาต์อื่นๆ มาวางลงบนเลย์เอาต์ที่วางไว้ก่อนหน้านี้ อย่างเช่น<br />

เลือก TableRow ซึ่งจะวาง Views และ ViewGroups ในแนวนอน ในตัวอย่างนี้เราจะ<br />

วางทั้งหมด 3 อัน<br />

3. คลิกขวาที่ TableRow แต่ละตัว และเพิ่มอีลีเมนต์ View จากตัวเลือก View อย่างเช่น<br />

เพิ่ม Button และ CheckBox ลงใน TableRow ตัวแรก เพิ่ม TextViews ลงใน<br />

TableRow ตัวที่ 2 และเพิ่ม TimePicker ลงใน TableRow ตัวที่ 3<br />

4. เพิ่ม Spinner และ VideoView ที่ข้างใต้ TableRow<br />

ผลที่ได้จะเป็นดังรูปที่ 4.2 ซึ่งเราสามารถแสดงหน้าจอให้เป็นแบบแนวตั้งหรือแนวนนอนเพื่อดู<br />

ความแตกต่างของการแสดงเลย์เอาต์ได้ หลังจากนั้นให้คลิกที่ main.xml จะเห็นว่าภายในไฟล์จะ<br />

แสดงคำสั่ง XML ตามชุคคำสั่งที่ 4.1 ซึ่งแสดงถึงเลย์เอาต์ที่เราได้ออกแบบเอาไว้<br />

ชุดคำสั่งที่ 4.1 main.xml<br />

วิวและกลุ่มของวิว<br />

83<br />

<br />

<br />

<br />


84 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

android:layout_width="wrap_content"<br />

android:layout_height="wrap_content" /><br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

อีกหนึ่งวิธีสำหรับการดูเลย์เอาต์ก็คือการใช้ Hierarchy Viewer ถ้ารันแอพภายใต้ระบบจำลอง<br />

(Emulator) เราจะใช้คำสั่ง hierarchyviewer ได้จากทางคอมมานด์ไลน์ คำสั่งนี้อยู่ในไดเร็กทอรี<br />

tools/ ของ Android SDK และด้วยเหตุผลทางด้านความปลอดภัย เครื่องมือนี้จึงทำงานบนระบบ<br />

จำลองเท่านั้น เพราะการใช้ Hierarchy Viewer บนอุปกรณ์แอนดรอยด์จริงๆ นั้นมักเกิดข้อผิดพลาด<br />

เกี่ยวกับระบบความปลอดภัยในอุปกรณ์แต่ละตัว จากนั้นให้คลิกเลือกที่หน้าต่างและเลือก Load View<br />

Hierarchy หน้าต่างแสดงความสัมพันธ์ของออบเจ็กต์ต่างๆ ในเลย์เอาต์จะเปิดขึ้นมา ให้ลองดูรูปที่<br />

4.3 ประกอบกัน


วิวและกลุ่มของวิว<br />

85<br />

รูปที่ 4.2 ตัวอย่างของตัวสร้างเลย์เอาต์เมื่อแสดงใน Eclipse<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

TableRow<br />

TableRow<br />

TableRow<br />

TableRow<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

id/TableRow03<br />

id/TableRow03<br />

id/TableRow03<br />

id/TableRow03<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

#2@43 afdbB<br />

id/TableRow03<br />

TableRow<br />

TableRow<br />

TableRow<br />

TableRow<br />

TableRow<br />

TableRow<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

#2@43 afdbB<br />

id/TableRow03<br />

id/TableRow03<br />

id/TableRow03<br />

id/TableRow03<br />

id/TableRow03<br />

id/TableRow03<br />

รูปที่ 4.3 ตัวอย่างของ Hierarchy เมื่อแสดงจากคำาสั่งในชุดคำาสั่งที่ 4.1


86 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

กรรมวิธี: การควบคุมความสูงและความกว้างของวัตถุบนจอภาพ<br />

ชุดคำสั่งนี้จะแสดงวิธีการควบคุมความสูงและความกว้างของอีลีเมนต์ต่างๆ บนจอให้มีผลกับทุก<br />

เลย์เอาต์ที่เราสร้างไว้ ออบเจ็กต์ View แต่ละตัวสามารถกำหนดความกว้างโดยรวมด้วย<br />

android:layout_width และกำหนดความสูงโดยรวมด้วย android:layout_height ซึ่งการ<br />

กำหนดค่าเหล่านี้ทำได้ 3 แบบคือ<br />

m exact dimension – กำหนดค่าขนาดจริงซึ่งจะไม่สามารถปรับขนาดได้เมื่อแสดงใน<br />

จอภาพขนาดต่างๆ กัน<br />

m wrap_content – กำหนดค่าให้มีขนาดใหญ่เพียงพอที่จะครอบคลุมอีลีเมนต์ รวมทั้งพื้นที่<br />

ว่างที่อยู่รอบๆ อีลีเมนต์<br />

m fill_parent – กำหนดค่าสูงสุดเพื่อครอบคลุมอีลีเมนต์ทั้งหมด รวมถึงพื้นที่ว่างที่อยู่รอบๆ<br />

อีลีเมนต์<br />

พื้นที่ว่างที่อยู่รอบๆ อีลีเมนต์ เราจะเรียกว่าแพดดิ้ง (Padding) มีค่าเริ่มต้นเป็นศูนย์ เป็นส่วน<br />

หนึ่งที่จะต้องนำมารวมเมื่อต้องการกำหนดขนาดของอีลีเมนต์ และจะต้องกำหนดด้วยค่าขนาดจริง<br />

โดยการกำหนดนั้นจะใช้แอททริบิวต์ 2 ชนิดดังนี้<br />

m padding – กำหนดค่าของแพดดิ้งให้มีค่าเท่ากับขนาดทั้ง 4 ด้านของอีลีเมนต์<br />

m paddingLeft, paddingRight, paddingTop, paddingBottom -- กำหนดค่าของ<br />

แพดดิ้งให้มีค่าเท่ากับด้านทั้ง 4 ด้านของอีลีเมนต์ ซึ่งสามารถกำหนดค่าเหล่านี้แยกจาก<br />

กันได้<br />

ส่วนแอททริบิวต์ android:layout_weight จะกำหนดค่าด้วยตัวเลข ซึ่งค่านี้จะแสดงถึงลำดับ<br />

ความสำคัญของอีลีเมนต์แต่ละตัวที่แสดงอยู่บนเลย์เอาต์เดียวกันเพื่อให้ระบบปฏิบัติการสามารถล ำดับ<br />

การทำงานได้อย่างถูกต้อง<br />

ชุดคำสั่งที่ 4.2 จะแสดงข้อมูลในไฟล์เลย์เอาต์หลัก ซึ่งประกอบไปด้วยปุ่มจำนวน 4 ปุ่ม โดยจะจัด<br />

เรียงปุ่มเป็นแนวนอน ดังรูปที่ 4.4<br />

ชุดคำสั่งที่ 4.2 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />


วิวและกลุ่มของวิว<br />

87<br />

รูปที่ 4.4 เลย์เอาต์แบบ LinearLayout พร้อมปุ่ม 4 ปุ่ม<br />

เรียงเป็นแนวนอนตามชุดคำาสั่งที่ 4.2<br />

ถ้าความสูงของปุ่ม “add” ถูกเปลี่ยนไปเป็น fill_parent ปุ่มนี้ก็จะเพิ่มพื้นที่ว่างในแนวดิ่งเพื่อ<br />

ปรับข้อความบนปุ่มให้อยู่ตรงกลางเสมอ และถ้าความกว้างของปุ่มมีการเปลี่ยนแปลง ปุ่มอื่นๆ ที่อยู่ต่อ<br />

ท้ายก็จะไม่ปรากฏเพราะเกินขนาดของจอ (ดังรูปที่ 4.5)<br />

รูปที่ 4.5 ค่าความสูงแบบ fill_parent ยังคงรักษาการจัดวางในแนวนอนของปุ่มอื่นไว้<br />

แต่ถ้าใช้ค่านี้กับความกว้าง ปุ่มอื่นๆ จะหายไปตามชุดคำาสั่งที่ 4.2<br />

จากรูปที่ 4.4 จะเห็นว่าข้อความปุ่ม multiply และปุ่ม divide แสดงได้ไม่ครบถ้วน ปัญหานี้แก้<br />

ได้ด้วยการเพิ่มที่ว่างให้ข้อความบนปุ่ม หรืออาศัยความหลากหลายของการจัดรูปแบบของปุ่มและ<br />

ข้อความมาช่วยก็ได้ ดังรูปที่ 4.6<br />

รูปที่ 4.6 หลากหลายรูปแบบในการจัดเรียงปุ่ม 4 ปุ่ม


88 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

การจัดเรียงปุ่มทั้ง 4 ปุ่มในรูปที่ 4.6 มีรายละเอียดดังนี้<br />

m ปุ่มแถวที่ 1 แสดงตามชุดคำสั่งที่ 4.2 แต่จะเพิ่มช่องว่างหลังคำแต่ละคำ<br />

m ปุ่มแถวที่ 2 มีการเปลี่ยนแปลงความกว้างเป็น fill_parent ตรงปุ่มสุดท้ายเพื่อเพิ่มที่<br />

ว่างภายในปุ่ม แต่จะไม่สามารถเพิ่มค่านี้ในปุ่มอื่นๆ ได้อีก เพราะอาจทำให้การแสดงผล<br />

ผิดพลาดได้ (ดังรูปที่ 4.5)<br />

<br />

m ปุ่มแถวที่ 3 มีการเพิ่มแพดดิ้งให้ปุ่ม multiply ทำให้ปุ่มใหญ่ขึ้น แต่ไม่ได้เพิ่มที่ว่างให้กับ<br />

ข้อความบนปุ่ม เพราะเลย์เอาต์ถูกประกาศด้วย wrap_content<br />

<br />

m ปุ่มแถวที่ 4 ใช้ fill_parent กับปุ่มทั้ง 4 นอกจากนี้ยังเพิ่ม layout_weight<br />

และกำหนดค่าให้แก่ปุ่มทุกปุ่มด้วยค่าเดียวกัน จะให้ผลลัพธ์ที่ดูเหมาะสมที่สุด<br />

<br />

<br />

<br />


วิวและกลุ่มของวิว<br />

กรรมวิธี: การกำหนดความสัมพันธ์ระหว่างเลย์เอาต์และเลย์เอาต์ไอดี<br />

ในบางครั้งการกำหนดเลย์เอาต์แสดงตำแหน่งแบบสัมพันธ์ให้กับออบเจ็กต์ที่เริ่มต้นทำงานอาจดู<br />

สะดวกมากกว่าการกำหนดเลย์เอาต์แบบใช้ค่าโดยตรง ถ้าออบเจ็กต์ UI เริ่มต้นทำงานด้วย Linear-<br />

Layouts มันจะถูกสั่งให้แสดงผลแบบสัมพันธ์ ในการกำหนดความสัมพันธ์นี้ เราจะใช้วิว Relative-<br />

Layout ตามที่แสดงในชุดคำสั่งที่ 4.3 ส่วนผลลัพธ์ของเลย์เอาต์ ดูได้ในรูปที่ 4.7<br />

89<br />

ชุดคำสั่งที่ 4.3 ตัวอย่างของ RelativeLayout<br />

รูปที่ 4.7 ตัวอย่างการใช้ RelativeLayout ร่วมกับข้อความ<br />

<br />

<br />

<br />

<br />

<br />

<br />


90 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

คำอธิบายเกี่ยวกับแอททริบิวต์เหล่านี้และกฎที่ใช้ในการระบุความสัมพันธ์ของเลย์เอาต์จะแสดง<br />

อยู่ในตารางที่ 4.2 เนื่องจากการประกาศเลย์เอาต์นั้นสามารถทำได้ทั้งในไฟล์ XML หรือไฟล์จาวา<br />

จึงต้องใช้ค่าอ้างอิงที่แตกต่างกัน โดยข้อมูลใน 3 บรรทัดแรกของตารางจะแสดงถึงแอททริบิวต์ที่ต้อง<br />

ใช้ร่วมกับ View ID ส่วนข้อมูลใน 2 บรรทัดท้ายของตารางจะมีค่าเป็นบูลีน (Boolean)<br />

ตารางที่ 4.2 กฎของการใช้เลย์เอาต์แบบสัมพันธ์ (Relative Layout)<br />

Relative Layout Rule XML แอททริบิวต์(ต้องขึ้นต้นด้วย<br />

android: เสมอ)<br />

จัดขอบของวิวให้สัมพันธ์<br />

layout_above<br />

กับขอบของวิวที่ตรึงไว้ layout_below<br />

จัดขอบของวิวด้วยค่าของ<br />

ขอบวิวที่ตรึงไว้<br />

จัดข้อความของวิวให้<br />

สัมพันธ์กับขอข้อความของ<br />

วิวที่ตรึงไว้<br />

จัดขอบของวิวให้สัมพันธ์<br />

กับขอบของวิว parent<br />

จัดวิวให้อยู่กึ่งกลางของ<br />

วิว parent<br />

layout_toRightOf<br />

layout_toLeftOf<br />

layout_alignTop<br />

layout_alignBottom<br />

layout_alignRight<br />

layout_alignLeft<br />

layout_alignBaseline<br />

layout_alignParentTop<br />

layout_alignParentBottom<br />

layout_alignParentRight<br />

layout_alignParentLeft<br />

layout_centerInParent<br />

layout_centerHorizontal<br />

layout_centerVertical<br />

Java Constant<br />

ABOVE<br />

BELOW<br />

RIGHT_OF<br />

LEFT_OF<br />

ALIGN_TOP<br />

ALIGN_BOTTOM<br />

ALIGN_RIGHT<br />

ALIGN_LEFT<br />

ALIGN_BASELINE<br />

ALIGN_PARENT_TOP<br />

ALIGN_PARENT_BOTTOM<br />

ALIGN_PARENT_RIGHT<br />

ALIGN_PARENT_LEFT<br />

CENTER_IN_PARENT<br />

CENTER_HORIZONTAL<br />

CENTER_VERTICAL<br />

กรรมวิธี: การสร้างเลย์เอาต์โดยการเขียนโปรแกรม<br />

เฟรมเวิร์คเลย์เอาต์ของระบบปฏิบัติการแอนดรอยด์ถูกพัฒนาขึ้นเพื่อรองรับการแสดงผลบน<br />

จอภาพหลายชนิด และมีเมธอดที่ช่วยในการสร้างเลย์เอาต์อยู่หลายตัว ในบางครั้งเพื่อความสะดวกใน<br />

การทำงาน เลยอยากเปลี่ยนค่าของเลย์เอาต์ด้วยการเขียนโปรแกรมจาวาก็สามารถทำได้


จากเลย์เอาต์ในชุดคำสั่งที่แล้ว เราเขียนด้วยภาษาจาวาเหมือนในชุดคำสั่งที่ 4.4 การสร้าง<br />

เลย์เอาต์เช่นนี้จะง่ายต่อการสร้าง แต่จะขาดคุณสมบัติในแง่การใช้งานแบบโปรแกรมมิ่ง ซึ่งจะเป็น<br />

ประโยชน์ในกรณีที่มีการใช้งานเลย์เอาต์ร่วมกันในหลายๆ แอพ โดยที่ก่อนหน้านี้เราได้ทำการแยกส่วน<br />

ของเลย์เอาต์และชุดคำสั่งออกจากกันไว้แล้ว<br />

ชุดคำสั่งที่ 4.4 src/com/cookbook/programmaticlayout/ProgrammaticLayout.java<br />

package com.cookbook.programmatic_layout;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.ViewGroup;<br />

import android.view.ViewGroup.LayoutParams;<br />

import android.widget.RelativeLayout;<br />

import android.widget.TextView;<br />

public class ProgrammaticLayout extends Activity {<br />

private int TEXTVIEW1_ID = 100011;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

วิวและกลุ่มของวิว<br />

//Here is an alternative to: setContentView(R.layout.main);<br />

final RelativeLayout relLayout = new RelativeLayout( this );<br />

relLayout.setLayoutParams( new RelativeLayout.LayoutParams(<br />

LayoutParams.FILL_PARENT,<br />

LayoutParams.FILL_PARENT ) );<br />

TextView textView1 = new TextView( this );<br />

textView1.setText("middle");<br />

textView1.setTag(TEXTVIEW1_ID);<br />

RelativeLayout.LayoutParams text1layout = new<br />

RelativeLayout.LayoutParams( LayoutParams.WRAP_CONTENT,<br />

LayoutParams.WRAP_CONTENT );<br />

text1layout.addRule( RelativeLayout.CENTER_IN_PARENT );<br />

relLayout.addView(textView1, text1layout);<br />

TextView textView2 = new TextView( this );<br />

textView2.setText("high");<br />

RelativeLayout.LayoutParams text2Layout = new<br />

RelativeLayout.LayoutParams( LayoutParams.WRAP_CONTENT,<br />

LayoutParams.WRAP_CONTENT );<br />

text2Layout.addRule(RelativeLayout.ABOVE, TEXTVIEW1_ID );<br />

relLayout.addView( textView2, text2Layout );<br />

91<br />

}<br />

}<br />

setContentView( relLayout );


92 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

กรรมวิธี: การใช้เธรดเพื่ออัพเดตเลย์เอาต์<br />

ในบทที่ 3 “เธรด เซอร์วิส รีซีฟเวอร์ และการแจ้งเตือน” เราได้พูดถึงเกี่ยกับการนำเธรดมาใช้<br />

ในแอพเพื่อรองรับการทำงานประเภทที่ต้องใช้ระยะเวลาในการประมวลผล และแสดงการทำงานบน<br />

จอภาพเพื่อให้ผู้ใช้รู้ว่าแอพยังคงทำงานอยู่โดยที่ให้ความสำคัญกับการแสดงผลบนจอภาพเป็นอันดับ<br />

แรกกันไปแล้ว<br />

ตอนนี้เราจะมาใช้ปุ่มสั่งการทำงานของงาน 2 ส่วน และทำการอัพเดตจอภาพเมื่องานใดงาน<br />

หนึ่งประมวลผลเสร็จ โดยในชุดคำสั่งที่ 4.5 ได้แสดงเลย์เอาต์ที่จะใช้ในชุดคำสั่งนี้เอาไว้ ซึ่งจะมี<br />

ข้อความแสดงสถานะการทำงานชื่อ computation_status และปุ่มที่ใช้สั่งให้ทำงาน ชื่อ action<br />

โดยเลย์เอาต์จะใช้ข้อความแสดงผลตามที่กำหนดไว้ในไฟล์ strings.xml ตามที่แสดงในชุดคำสั่งที่ 4.6<br />

ชุดคำสั่ง 4.5 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

ชุดคำสั่ง 4.6 res/layout/strings.xml<br />

<br />

<br />

Hello World, HandlerUpdateUi!<br />

HandlerUpdateUi<br />

Press to Start<br />

Starting...<br />

First Done<br />

Second Done<br />

<br />

ขั้นตอนในการอัพเดตออบเจ็กต์ UI โดยใช้เธรดแบบทำงานเบื้องหลังมีดังนี้<br />

1. สั่งให้ออบเจ็กต์ UI ที่ต้องการอัพเดตด้วยเธรดเริ่มทำงาน (ในที่นี้จะเรียกว่า av)


2. กำหนดฟังก์ชั่นที่จะใช้ในการอัพเดตออบเจ็กต์ UI (ในที่นี้จะเรียกว่า mUpdateResults)<br />

3. กำหนดแฮนด์เลอร์เพื่อรับส่งข้อมูลระหว่างเธรด (ในที่นี้จะเรียกว่า mHandler)<br />

4. กำหนดแฟล็กไว้ในเธรดเพื่อใช้แสดงสถานะของเธรด (ในที่นี่เราจะเปลี่ยนแปลงค่าของ<br />

text_string และ background_color)<br />

5. ส่งแฮนด์เลอร์จากเธรดเพื่ออัพเดตข้อมูลในออบเจ็กต์ UI<br />

ขั้นตอนทั้งหมดนี้แสดงไว้ในชุดคำสั่งที่ 4.7<br />

ชุดคำสั่ง 4.7 src/com/cookbook/handler_ui/HandlerUpdateUi.java<br />

package com.cookbook.handler_ui;<br />

import android.app.Activity;<br />

import android.graphics.Color;<br />

import android.os.Bundle;<br />

import android.os.Handler;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class HandlerUpdateUi extends Activity {<br />

TextView av; //UI reference<br />

int text_string = R.string.start;<br />

int background_color = Color.DKGRAY;<br />

final Handler mHandler = new Handler();<br />

// Create runnable for posting results to the UI thread<br />

final Runnable mUpdateResults = new Runnable() {<br />

public void run() {<br />

av.setText(text_string);<br />

av.setBackgroundColor(background_color);<br />

}<br />

};<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

av = (TextView) findViewById(R.id.computation_status);<br />

วิวและกลุ่มของวิว<br />

Button actionButton = (Button) findViewById(R.id.action);<br />

actionButton.setOnClickListener(new View.OnClickListener() {<br />

93


94 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

}<br />

public void onClick(View view) {<br />

do_work();<br />

}<br />

});<br />

//example of a computationally intensive action with UI updates<br />

private void do_work() {<br />

Thread thread = new Thread(new Runnable() {<br />

public void run() {<br />

text_string=R.string.start;<br />

background_color = Color.DKGRAY;<br />

mHandler.post(mUpdateResults);<br />

computation(1);<br />

text_string=R.string.first;<br />

background_color = Color.BLUE;<br />

mHandler.post(mUpdateResults);<br />

}<br />

computation(2);<br />

text_string=R.string.second;<br />

background_color = Color.GREEN;<br />

mHandler.post(mUpdateResults);<br />

}<br />

});<br />

thread.start();<br />

}<br />

final static int SIZE=1000; //large enough to take some time<br />

double tmp;<br />

private void computation(int val) {<br />

for(int ii=0; ii


การจัดการข้อความ<br />

95<br />

จากการวางโครงสร้างข้างต้นจะทำให้ข้อความต่างๆ ที่ใช้ในแอพถูกจัดเก็บไว้ในที่เดียว การเพิ่ม<br />

ข้อความลงในอีลีเมนต์ UI เช่น TextView จะใช้คำสั่งดังนี้<br />

<br />

ฟอนต์ที่ใช้ในการแสดงข้อความนั้นจะขึ้นอยู่กับอุปกรณ์แอนดรอยด์ที่ใช้งาน และค่าต่างๆ<br />

ที่ผู้ใช้ได้กำหนดไว้ การกำหนดฟอนต์ที่จะใช้งานในแอพนั้น เราสามารถเลือกใช้อีลีเมนต์ตามที่แสดง<br />

ในตารางที่ 4.3 ได้<br />

ตารางที่ 4.3 แอททริบิวต์ของ TextView (ค่าเริ่มต้นของแอททริบิวต์จะแสดงด้วยตัวหนาใน<br />

คอลัมน์สุดท้าย)<br />

แอททริบิวต์ของ<br />

TextView<br />

XML Element Java Method ค่าที่สามารถกาหนดได้<br />

และค่าเริ่มต้น<br />

ข้อความที่จะแสดง android:text setText(CharSequence) ข้อความต่างๆ<br />

ขนาดของฟอนต์ android:textSize setTextSize(float) ขนาดของฟอนต์<br />

สีของฟอนต์ android:textColor setTextColor (int) สีต่าง<br />

สีพื้นหลัง ไม่มี setBackgroundColor(int) สีต่างๆ<br />

สไตล์ของฟอนต์ android:textStyle setTypeface (Typeface) ตัวหนา<br />

ตัวเอน<br />

ตัวหนาเอน<br />

ชนิดของฟอนต์ android:typeface setTypeface (Typeface) ปกติ<br />

san<br />

serif<br />

monospace<br />

ตำาแหน่งของข้อความ android:gravity setGravity(int) บน<br />

ล่าง<br />

ซ้าย<br />

ขวา<br />

(อื่นๆ)<br />

กรรมวิธี: การกำหนดและเปลี่ยนแปลงคุณลักษณะของข้อความ<br />

ในส่วนนี้จะแสดงการเปลี่ยนสีของข้อความเมื่อมีการกดปุ่ม นอกเหนือจากการเปลี่ยนสีของ<br />

ข้อความแล้ว เรายังสามารถเปลี่ยนขนาดของฟอนต์และรูปแบบของฟอนต์ด้วยได้<br />

ไฟล์เลย์เอาต์หลักได้กำหนดให้ TextView และ Button แสดงในแนวดิ่ง และกำหนดเป็น<br />

LinearLayout ตามที่แสดงในชุดคำสั่งที่ 4.8 ซึ่งข้อความที่จะใช้แสดงนั้น อยู่ในชุดคำสั่งที่ 4.9<br />

โดยปุ่มจะแสดงข้อความจาก button_text ที่เก็บอยู่ในไฟล์ XML


96 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

ชุดคำสั่งที่ 4.8 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

ชุดคำสั่งที่ 4.9 res/values/strings.xml<br />

<br />

<br />

ChangeFont<br />

Rainbow Connection<br />

Press to change the font color<br />

<br />

แอคทิวิตี้ที่แสดงในชุดคำสั่งที่ 4.10 จะทำงานร่วมกับไฟล์ main.xml และเมื่อปุ่มถูกกด ฟัง<br />

ก์ชั่น OnClickListener จะกำหนดสีให้แก่ข้อความตามค่าอ้างอิงในตารางที่ 4.3 โดยสีที่จะใช้งาน<br />

สามารถประกาศไว้ในไฟล์ colors.xml ตามที่แสดงในชุดคำสั่งที่ 4.11 ได้เช่นกัน การกำหนดค่าของสี<br />

ไว้ในไฟล์ XML ภายนอกนั้น จะช่วยให้เราสามารถแก้ไขสีของข้อความได้ง่ายโดยไม่จำเป็นต้องแก้ไข<br />

การทำงานของแฮนด์เลอร์<br />

ชุดคำสั่งที่ 4.10 src/com/cookbook/change_font/ChangeFont.java<br />

package com.cookbook.change_font;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class ChangeFont extends Activity {<br />

TextView tv;<br />

private int color_vals[]={R.color.start, R.color.mid, R.color.last};<br />

int idx=0;


** Called when the activity is first created. */<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.mod_text);<br />

การจัดการข้อความ<br />

97<br />

}<br />

}<br />

Button changeFont = (Button) findViewById(R.id.change);<br />

changeFont.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

tv.setTextColor(getResources().getColor(color_vals[idx]));<br />

idx = (idx+1)%3;<br />

}<br />

});<br />

ชุดคำสั่งที่ 4.11 res/values/colors.xml<br />

<br />

<br />

#f00<br />

#0f0<br />

#00f<br />

<br />

ชุดคำสั่งนี้สามารถแก้ไขเพิ่มเติมเพื่อให้สามารถเปลี่ยนขนาดของข้อความได้ อย่างเช่นว่าเราจะ<br />

เปลี่ยน color_vals[] ไปเป็น size_vals[] และประกาศค่าไว้ในรีซอร์ส R.dimen<br />

private int size_vals[]={R.dimen.small, R.dimen.medium, R.dimen.large};<br />

tv.setTextSize(getResources().getDimension(size_vals[idx]));<br />

เช่นเดียวกับไฟล์ colors.xml นั่นคือเราจะต้องสร้างไฟล์ dimens.xml เพื่อใช้งานเช่นกัน<br />

ดังแสดงไว้ในชุดคำสั่งที่ 4.12<br />

ชุดคำสั่งที่ 4.12 ตัวอย่างของการใช้ไฟล์ dimens.xml<br />

<br />

<br />

12sp<br />

24sp<br />

48sp<br />

<br />

การใช้ชุดคำสั่งนี้เพื่อเปลี่ยนขนาดของข้อความ เราต้องเปลี่ยนคำสั่งจากเดิม color_vals[]<br />

เป็น text_vals[] ดังนี้<br />

private int text_vals[]={R.string.first_text,<br />

R.string.second_text, R.string.third_text};<br />

tv.setText(getBaseContext().getString(text_vals[idx]));


98 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

ในการทำงานของแอพนั้น เราจะต้องใช้ไฟล์ strings.xml ดังแสดงในชุดคำสั่งที่ 4.13<br />

ชุดคำสั่งที่ 4.13 ตัวอย่างของการใช้ไฟล์ strings.xml<br />

<br />

<br />

ChangeFont<br />

Rainbow Connection<br />

Press To Change the Font Color<br />

First<br />

Second<br />

Third<br />

<br />

กรรมวิธี: การกรอกข้อความ<br />

เราจะใช้คลาส EditText ในการสร้างวิวเพื่อให้ผู้ใช้กรอกข้อมูล โดยเราจะต้องประกาศ<br />

อินสแตนซ์ของคลาสเช่นเดียวกับการใช้งานคลาส TextView สำหรับค่าแอททริบิวต์ของ EditText<br />

นั้นดูได้จากตารางที่ 4.4<br />

ตารางที่ 4.4 แอททริบิวต์ของ EditText (ค่าเริ่มต้นของแอททริบิวต์จะแสดงด้วยตัวหนาในคอลัมน์<br />

สุดท้าย)<br />

แอททริบิวต์ของ EditText XML ค่าที่สามารถกาหนดได้ และค่าเริ่มต้น<br />

จำานวนบรรทัดตำ่ำสุดที่จะแสดง android:minLines ตัวเลขต่างๆ<br />

จำานวนบรรทัดมากสุดที่จะแสดง android:maxLines ตัวเลขต่างๆ<br />

คำาอธิบายประกอบฟิลด์ที่กรอกข้อความ android:hint ข้อความต่างๆ<br />

ชนิดของข้อมูลที่กรอก android:inputType text<br />

textCapSentences<br />

textAutoCorrect<br />

textAutoComplete<br />

textEmailAddress<br />

textNoSuggestions<br />

textPassword<br />

number<br />

phone<br />

date<br />

time<br />

(อื่นๆ)


การจัดการข้อความ<br />

ตัวอย่างเช่น เราใช้ไฟล์เลย์เอาต์ XML ที่สร้างไว้มาทำงานร่วมกับการกรอกข้อความ โดยจะ<br />

แสดงข้อความ “Type text here” ที่มีสีเทาซึ่งเป็นคำอธิบายของฟิลด์ข้อความนี้ สำหรับอุปกรณ์<br />

แอนดรอยด์ที่ไม่มีแป้นพิมพ์แบบฮาร์ดแวร์นั้น เมื่อมีการคลิกที่ฟิลด์เพื่อกรอกข้อความ ระบบจะแสดง<br />

แป้นพิมพ์แบบซอฟต์แวร์ขึ้นมาให้โดยอัตโนมัติ ดังรูปที่ 4.8<br />

<br />

99<br />

รูปที่ 4.8 ตัวอย่างของฟิลด์ที่ใช้กรอกข้อความและแป้นพิมพ์แบบซอฟต์แวร์


100 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

ถ้าเราใช้คำสั่ง android:inputType=”phone” แป้นพิมพ์แบบซอฟต์แวร์ก็จะแสดงแป้น<br />

ตัวเลขแบบที่ใช้ในการกรอกเบอร์โทรศัพท์ขึ้นมา หรือถ้าเป็น android:inputType=”textEmailAddress”<br />

แป้นพิมพ์ก็จะแสดงแป้นแบบที่ใช้ในการกรอกอีเมล์แอดเดรส พร้อมทั้งแสดงข้อความอธิบายของฟิลด์<br />

ดังแสดงในรูปที่ 4.9<br />

รูปที่ 4.9 ตัวอย่างของแป้นพิมพ์แบบซอฟต์แวร์เมื่อแสดงตามชนิดของข้อมูลที่จะกรอก<br />

การกรอกข้อมูลลงในฟิลด์นั้น เราสามารถใช้คำสั่งที่แสดงในตารางที่ 4.4 เพื่อกำหนดให้อักษรที่<br />

กรอกเปลี่ยนเป็นตัวเล็กหรือตัวใหญ่ได้ รวมทั้งตรวจสอบคำผิดหรือแสดงคำที่ใกล้เคียงในขณะที่พิมพ์<br />

ข้อความ ซึ่งสามารถเลือกใช้ได้ตามต้องการ<br />

กรรมวิธี: การสร้างฟอร์ม<br />

ฟอร์มเป็นเลย์เอาต์ชนิดหนึ่งที่เป็นขอบเขตของพื้นที่บนจอภาพที่แสดงฟิลด์กรอกข้อความ<br />

หลายๆ ฟิลด์เอาไว้เพื่อให้ผู้ใช้สามารถเลือกฟิลด์ที่จะกรอกข้อความได้ โดยเราจะใช้งานคลาส<br />

EditText ในการสร้างฟอร์ม ซึ่งแสดงในชุดคำสั่งที่ 4.14 จะเห็นว่าใน textResult ของตัวอย่างนี้จะ<br />

ไม่มีการแก้ไขอะไร


ชุดคำสั่งที่ 4.14 ตัวอย่างการรับข้อความจากออบเจ็กต์ EditText<br />

วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

101<br />

CharSequence phoneNumber;<br />

EditText textResult = (EditText) findViewById(R.id.text_result);<br />

textResult.setOnKeyListener(new OnKeyListener() {<br />

public boolean onKey(View v, int keyCode, KeyEvent event) {<br />

// register the text when "enter" is pressed<br />

if ((event.getAction() == KeyEvent.ACTION_DOWN) &&<br />

(keyCode == KeyEvent.KEYCODE_ENTER)) {<br />

// grab the text for use in the activity<br />

phoneNumber = textResult.getText();<br />

return true;<br />

}<br />

return false;<br />

}<br />

});<br />

เมื่อเมธอด onKey มีการส่งค่ากลับมาเป็น true นั่นหมายความว่าเหตุการณ์ของการกดปุ่มนั้น<br />

ได้ใช้งานอยู่แล้ว จึงไม่จำเป็นต้องทำงานในส่วนนี้อีก<br />

ถ้าต้องการแสดงตัวเลือกข้อมูลเพื่อให้ผู้ใช้ได้ทำการเลือก เราจะต้องใช้วิดเจ็ตประเภท<br />

เช็คบ็อกซ์, ปุ่มเรดิโอ หรือลิสต์แบบดร็อปดาวน์ ซึ่งเดี๋ยวเราค่อยพูดถึงในหัวข้อถัดไป<br />

วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

ระบบปฏิบัติการแอนดรอยด์มีวิดเจ็ตให้เลือกใช้ในการพัฒนาแอพอยู่หลายอย่าง วิดเจ็ตส่วน<br />

ใหญ่ที่ใช้กัน ได้แก่<br />

m Button – มีลักษณะเป็นปุ่มกดสี่เหลี่ยม สามารถแสดงข้อความหรือรูปภาพได้ และตอบ<br />

สนองต่อการกดเพื่อทำงานต่างๆ ที่ได้กำหนดไว้<br />

m CheckBox – มีลักษณะเป็นปุ่มพร้อมเครื่องหมายถูกและมีข้อความอธิบาย สามารถสลับ<br />

สถานะการกดและไม่กดได้ บางครั้งเรียกว่า ToggleButton<br />

m RadioButton – มีลักษณะเป็นปุ่มกลมๆ เหมือนจุด เลือกด้วยการแตะสัมผัส ปุ่มแบบนี้<br />

จะรองรับการเลือกได้แค่ปุ่มเดียวเท่านั้น ปุ่มที่ถูกเลือกจะมีสถานะเป็น On ส่วนปุ่มอื่นๆ<br />

ที่เหลือจะมีสถานะเป็น Off<br />

m Spinner – มีลักษณะเป็นปุ่มที่แสดงข้อมูลที่ผู้ใช้งานเลือกอยู่ในขณะนั้น และมีรูปลูกศร<br />

เพื่อแสดงรายการของตัวเลือกอื่นๆ บางครั้งจะเรียกว่า ComboListBox<br />

m ProgressBar - มีลักษณะเป็นแถบยาวตามแนวนอน ใช้เพื่อแสดงสถานะต่างๆ ในแบบ<br />

เปอร์เซ็นต์ของงานที่กำลังทำอยู่ วิดเจ็ตนี้มีหน้าที่แสดงผลอย่างเดียว ผู้ใช้ไม่สามารถทำ<br />

อะไรด้วยได้


102 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

m SeekBar – มีลักษณะเหมือน ProgressBar เพียงแต่สามารถลากและเปลี่ยนแปลงค่า<br />

ได้ มักใช้แสดงระยะเวลาที่ต้องใช้ในการเล่นไฟล์เสียง โดยผู้ใช้สามารถลากไปยังตำแหน่ง<br />

ต่างๆ บนแถบนี้เพื่อเลือกเล่นไฟล์เสียง ณ ตำแหน่งที่เลือกได้<br />

ในหัวข้อถัดไปจะแสดงตัวอย่างการใช้งานวิดเจ็ตต่างๆ ให้ดูกัน<br />

กรรมวิธี: การใช้งานปุ่มแบบรูปภาพ<br />

ในบทที่ 2 เราได้ศึกษาการใช้ปุ่มกันไปแล้ว ซึ่งเราสามารถใส่รูปภาพลงบนปุ่มได้ด้วยการกำหนด<br />

ค่าของแอททริบิวต์ android:background แต่ถ้าใช้วิดเจ็ตชื่อ ImageButton เราจะสามารถกำหนด<br />

รูปแบบของปุ่มได้มากขึ้น โดยเลือกรูปภาพที่จะใช้กับวิดเจ็ตนี้ด้วยแอททริบิวต์ android:src ดังนี้<br />

<br />

จากคำสั่งข้างต้น รูปภาพจะแสดงอยู่บนวิดเจ็ต โดย ImageButton จะใช้ค่าของรูปภาพตามที่<br />

กำหนดไว้ในวิดเจ็ต ImageView ด้วยคำสั่ง android:scaleType ดังรูปที่ 4.10<br />

รูปที่ 4.10 ตัวอย่างผลจากการใช้แอททริบิวต์<br />

android:scaleType


103<br />

คำสั่งอื่นๆ ที่เราสามารถนำมาใช้กับ ImageButton ได้ มีดังนี้<br />

m ใช้แอททริบิวต์ android:padding เพื่อป้องกันการแสดงปุ่มซ้อนกัน หรือใช้ในการกำหนด<br />

ที่ว่างระหว่างปุ่มอื่นๆ<br />

m กำหนดค่าแอททริบิวต์ android:background ให้มีค่าเป็นว่าง (null) เพื่อทำการซ่อนปุ่ม<br />

และแสดงเพียงรูปภาพเท่านั้น<br />

เมื่อปุ่มถูกซ่อนไว้ รูปภาพที่แสดงอยู่นั้นจะไม่มีเหตุการณ์ตอบสนองใดๆ รวมทั้งการเปลี่ยนแปลง<br />

ของภาพด้วย เราสามารถปรับแต่งเพิ่มเติมได้ด้วยการเพิ่มอีลีเมนต์ให้แก่ปุ่มดังนี้<br />

<br />

<br />

<br />

<br />

<br />

<br />

คำสั่งด้านบนกำหนดรูปภาพจำนวน 3 รูปให้แก่เหตุการณ์ของปุ่ม 3 เหตุการณ์คือ ตอนปกติ<br />

ตอนกดปุ่ม และตอนที่เลือก (Focus) โดยรูปภาพทั้ง 3 นี้จะกำหนดไว้ในไดเร็กทอรีรีซอร์ส res/<br />

drawable-mdpi/ และไฟล์เหล่านี้จะถูกกำหนดไว้ในแอททริบิวต์ android:src ของ ImageButton<br />

เมื่อมีการวางปุ่มหลายๆ ปุ่มลงในเลย์เอาต์เดียวกัน เราจะใช้วิว TableLayout มาช่วยในการ<br />

แสดงผล โดยจะมีการทำงานคล้ายกับ LinearLayout ตัวอย่างของเลย์เอาต์ที่แสดงในชุดคำสั่งที่<br />

4.15 จะแสดงการสร้างเลย์เอาต์เพื่อให้เห็นผลลัพธ์ดังรูปที่ 4.11<br />

ชุดคำสั่งที่ 4.15> res/layout/ibutton.xml<br />

วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

<br />

<br />

<br />

<br />


104 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />


วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

105<br />

<br />

<br />

<br />

รูปที่ 4.11 แสดง TableLayout ของ ImageButtons และ TextViews<br />

กรรมวิธี: การใช้งานเช็คบ็อกซ์และปุ่มแบบ Toggle<br />

วิดเจ็ต CheckBox สามารถกำหนดค่าสีของข้อมูลที่ถูกเลือกหรือนำกราฟิกต่างๆ มาแสดงผลได้<br />

ซึ่งจะทำให้แอพดูน่าสนใจมากขึ้นและลดความสับสนในการใช้งาน ถ้ามีการดึงกราฟิกมาช่วยในการ<br />

แสดงผลของ CheckBox เราก็จะใช้คำสั่ง setButtonDrawable() ในการกำหนดรูปภาพเหล่านั้น<br />

วิดเจ็ต CheckBox จะต้องมีการประกาศในไฟล์เลย์เอาต์ XML เสมอ ตามที่แสดงในชุดคำสั่งที่<br />

4.16 ซึ่งแอททริบิวต์ android:text จะทำหน้าที่แสดงลาเบลต่อจาก CheckBox<br />

ชุดคำสั่งที่ 4.16 res/layout/ckbox.xml<br />

<br />


106 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

<br />

<br />

<br />

<br />

<br />

<br />

วิวที่เรากำหนดไว้ในไฟล์เลย์เอาต์นั้นสามารถทำงานร่วมกับอินสแตนซ์ View ในไฟล์จาวาได้<br />

ตามที่แสดงไว้ในชุดคำสั่งที่ 4.17 เช็คบ็อกซ์ทั้ง 3 อันจะมีคำสั่ง onClickListener อยู่เพื่อคอยตรวจ<br />

จับความเปลี่ยนแปลง และคอยอัพเดตข้อมูลให้แก่ TextView ผลลัพธ์ของชุดคำสั่งนี้แสดงไว้ในรูปที่<br />

4.12<br />

ชุดคำสั่งที่ 4.17 src/com/cookbook/layout_widgets/CheckBoxExample.java<br />

package com.cookbook.layout_widgets;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.CheckBox;<br />

import android.widget.TextView;<br />

public class CheckBoxExample extends Activity {<br />

private TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.ckbox);<br />

tv = (TextView) findViewById(R.id.status);<br />

class Toppings {private boolean LETTUCE, TOMATO, CHEESE;}


วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

107<br />

final Toppings sandwichToppings = new Toppings();<br />

final CheckBox checkbox[] = {<br />

(CheckBox) findViewById(R.id.checkbox0),<br />

(CheckBox) findViewById(R.id.checkbox1),<br />

(CheckBox) findViewById(R.id.checkbox2)};<br />

}<br />

}<br />

checkbox[0].setOnClickListener(new OnClickListener() {<br />

@Override<br />

public void onClick(View v) {<br />

if (((CheckBox) v).isChecked()) {<br />

sandwichToppings.LETTUCE = true;<br />

} else {<br />

sandwichToppings.LETTUCE = false;<br />

}<br />

tv.setText(""+sandwichToppings.LETTUCE + " "<br />

+sandwichToppings.TOMATO + " "<br />

+sandwichToppings.CHEESE + " ");<br />

}<br />

});<br />

checkbox[1].setOnClickListener(new OnClickListener() {<br />

@Override<br />

public void onClick(View v) {<br />

if (((CheckBox) v).isChecked()) {<br />

sandwichToppings.TOMATO = true;<br />

} else {<br />

sandwichToppings.TOMATO = false;<br />

}<br />

tv.setText(""+sandwichToppings.LETTUCE + " "<br />

+sandwichToppings.TOMATO + " "<br />

+sandwichToppings.CHEESE + " ");<br />

}<br />

});<br />

checkbox[2].setOnClickListener(new OnClickListener() {<br />

@Override<br />

public void onClick(View v) {<br />

if (((CheckBox) v).isChecked()) {<br />

sandwichToppings.CHEESE = true;<br />

} else {<br />

sandwichToppings.CHEESE = false;<br />

}<br />

tv.setText(""+sandwichToppings.LETTUCE + " "<br />

+sandwichToppings.TOMATO + " "<br />

+sandwichToppings.CHEESE + " ");<br />

}<br />

});


108 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

รูปที่ 4.12 ตัวอย่างการทำางานของ CheckBox<br />

ที่สถานะของการเลือกและไม่เลือก<br />

ปุ่มประเภท Toggle นั้นจะทำงานคล้ายๆ กับเช็คบ็อกซ์ โดยเราสามารถนำเอาคำสั่งในชุดคำสั่ง<br />

ที่ 4.16 มาทำการแก้ไขด้วยการแทนที่คำสั่ง CheckBox ด้วยคำสั่ง ToggleButton ได้ดังนี้<br />

<br />

จะเห็นว่าคำสั่ง android:test จะถูกแทนที่ด้วยคำสั่ง android:textOff (ถ้าไม่มีการกำหนด<br />

ค่าใดๆ ไว้จะมีค่าเป็น Off) และคำสั่ง android:textOn (ถ้าไม่มีการกำหนดค่าใดๆ ไว้จะมีค่าเป็น On)<br />

เพื่อใช้ในการแสดงผลลัพธ์ตามสถานะของการกดปุ่ม ToggleButton ตัวอย่างของผลลัพธ์จะแสดงอยู่<br />

ในรูปที่ 4.13<br />

กรรมวิธี: การใช้งานปุ่มตัวเลือกแบบเรดิโอ<br />

ปุ่มตัวเลือกแบบเรดิโอมีลักษณะเป็นกลุ่มของปุ่ม ซึ่งผู้ใช้จะต้องเลือกแค่ปุ่มใดปุ่มหนึ่ง เพราะปุ่ม<br />

ชนิดนี้จะรองรับการกดปุ่มเพียงปุ่มเดียว มีสถานะเป็น On และปุ่มอื่นๆ จะมีสถานะเป็น Off กลุ่มของ<br />

ปุ่มเหล่านี้จะเรียกว่า RadioGroup เพื่อกำหนดขอบเขตของการเลือกปุ่มไว้ เลย์เอาต์ของชุดคำสั่งนี้จะ<br />

แสดงอยู่ในชุดคำสั่งที่ 4.18


วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

109<br />

ชุดคำสั่งที่ 4.18 res/layout/rbutton.xml<br />

รูปที่ 4.13 ตัวอย่างการใช้งานปุ่มแบบ Toggle<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

ผลลัพธ์ของการนำชุดคำสั่งที่ 4.17 มาแก้ไขโดยแทนที่คำสั่ง CheckBox ด้วยคำสั่ง RadioButton<br />

แทน จะได้ผลลัพธ์ดังรูปที่ 4.14


110 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

รูปที่ 4.14 ตัวอย่างของการใช้ RadioGroup<br />

เพื่อแสดงกลุ่มของปุ่มแบบเรดิโอจำานวน 3 ปุ่ม<br />

กรรมวิธี: การสร้างเมนูแบบดร็อปดาวน์<br />

บางครั้งอาจเรียก Spinner ว่าเมนูแบบดร็อปดาวน์ (Drop-Down) สำหรับวิดเจ็ตตัวนี้เราจะ<br />

กำหนดไว้ในไฟล์เลย์เอาต์ปกติตามที่แสดงไว้ในชุดคำสั่งที่ 4.19<br />

ชุดคำสั่งที่ 4.19 res/layout/spinner.xml<br />

<br />

<br />

<br />

<br />

ข้อความไตเติ้ลที่จะแสดงในเมนูดร็อปดาวน์จะถูกกำหนดไว้ในแอททริบิวต์ android:prompt<br />

โดยข้อความนี้จะต้องกำหนดไว้ในไฟล์ strings.xml ตัวอย่างเช่น<br />

Choose your favorite ocean<br />

เมนูดร็อปดาวน์นี้จำเป็นต้องใช้ไฟล์เลย์เอาต์เพิ่มเติมเพื่อใช้ในการแสดงเมนูดร็อปดาวน์<br />

ซึ่งในชุดคำสั่งที่ 4.20 จะแสดงให้เห็นถึงข้อมูลภายในไฟล์ spinner_entry.xml<br />

ชุดคำสั่งที่ 4.20 res/layout/spinner_entry.xml<br />

<br />


android:gravity="center"<br />

android:textColor="#000"<br />

android:textSize="40sp"<br />

android:layout_width="fill_parent"<br />

android:layout_height="wrap_content"><br />

<br />

วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

111<br />

เลย์เอาต์ที่ใช้ในเมนูดร็อปดาวน์ นอกจากจะใช้กับข้อมูลประเภทข้อความแล้ว ยังสามารถใส่<br />

รูปภาพหรือออบเจ็กต์อื่นๆ ไว้ในเลย์เอาต์นี้ได้ด้วย<br />

ขั้นตอนการทำงานที่จะใช้สั่งให้เมนูดร็อปดาวน์ทำงานได้นั้น เราต้องประกาศ Adapter เพื่อรวม<br />

เอาเมนูดร็อปดาวน์และเลย์เอาต์ของเมนูดร็อปดาวน์มาแสดงในเลย์เอาต์หลักของแอพ ดังในชุดคำสั่ง<br />

ที่ 4.21<br />

ชุดคำสั่งที่ 4.21 src/com/cookbook/layout_widgets/SpinnerExample.java<br />

package com.cookbook.layout_widgets;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.widget.ArrayAdapter;<br />

import android.widget.Spinner;<br />

public class SpinnerExample extends Activity {<br />

private static final String[] oceans = {<br />

"Pacific", "Atlantic", "Indian",<br />

"Arctic", "Southern" };<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.spinner);<br />

Spinner favoriteOcean = (Spinner) findViewById(R.id.spinner);<br />

}<br />

}<br />

ArrayAdapter mAdapter = new<br />

ArrayAdapter(this, R.layout.spinner_entry, oceans);<br />

mAdapter.setDropDownViewResource(R.layout.spinner_entry);<br />

favoriteOcean.setAdapter(mAdapter);<br />

ในตัวอย่างก่อนหน้านี้ จะเห็นว่าเรากำหนดข้อมูลที่จะใช้เป็นตัวเลือกให้แก่เมนูดร็อปดาวน์ด้วย<br />

ตัวแปรที่ชื่อว่า oceans[] ซึ่งจะทำการส่งผ่านค่าไปยัง ArrayAdapter การทำงานเช่นนี้จะระบุการ<br />

ทำงานของเมนูดร็อปดาวน์ว่าไม่มีการเปลี่ยนแปลงค่าใดๆ ของเมนูดร็อปดาวน์ในระหว่างการทำงาน<br />

ของแอพ ส่วนในกรณีที่เราต้องการให้รายการของเมนูดร็อปดาวน์สามารถเพิ่มเติมหรือลบออกได้นั้น<br />

ให้ใช้คำสั่ง add()


112 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

ในส่วนของชุดคำสั่งด้านล่าง ส่วนที่แสดงเป็นตัวหนาในเมธอด onCreate() นั้น จะแสดงถึง<br />

การสร้างรายการของเมนูดร็อปดาวน์ดังนี้<br />

Spinner favoriteOcean = (Spinner) findViewById(R.id.spinner);<br />

ArrayAdapter mAdapter = new<br />

ArrayAdapter(this, R.layout.spinner_entry);<br />

mAdapter.setDropDownViewResource(R.layout.spinner_entry);<br />

for(int idx=0; idx


วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

113<br />

public class HandlerUpdateUi extends Activity {<br />

private static ProgressBar m_progressBar; //UI reference<br />

int percent_done = 0;<br />

final Handler mHandler = new Handler();<br />

// Create runnable for posting results to the UI thread<br />

final Runnable mUpdateResults = new Runnable() {<br />

public void run() {<br />

m_progressBar.setProgress(percent_done);<br />

}<br />

};<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

m_progressBar = (ProgressBar) findViewById(R.id.ex_progress_bar);<br />

}<br />

Button actionButton = (Button) findViewById(R.id.action);<br />

actionButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

do_work();<br />

}<br />

});<br />

//example of a computationally intensive action with UI updates<br />

private void do_work() {<br />

Thread thread = new Thread(new Runnable() {<br />

public void run() {<br />

percent_done = 0;<br />

mHandler.post(mUpdateResults);<br />

computation(1);<br />

percent_done = 50;<br />

mHandler.post(mUpdateResults);<br />

}<br />

computation(2);<br />

percent_done = 100;<br />

mHandler.post(mUpdateResults);<br />

}<br />

});<br />

thread.start();<br />

final static int SIZE=1000; //large enough to take some time<br />

double tmp;


}<br />

114 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

private void computation(int val) {<br />

for(int ii=0; ii


}<br />

วิดเจ็ตอื่นๆ: จากการใช้ปุ่มมาถึงการใช้ Seek Bar<br />

Thread initThread = new Thread(new Runnable() {<br />

public void run() {<br />

show_time();<br />

}<br />

});<br />

initThread.start();<br />

115<br />

int count;<br />

private void show_time() {<br />

for(count=0; count


116 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)<br />

รูปที่ 4.15 ตัวอย่างการใช้ Seek Bar<br />

ร่วมกับการใช้รูปภาพแทนทัมบ์


117<br />

บทที่ 5<br />

อีเวนต์ต่างๆ ที่เกิดขึ้น<br />

ในส่วนการติดต่อกับผู้ใช้งาน<br />

การทำงานในส่วนของการติดต่อกับผู้ใช้งานนั้นจะประกอบด้วย 2 ส่วน คือ ส่วนของเลย์เอาต์<br />

บนจอภาพ และส่วนของเหตุการณ์ต่างๆ (Event – อีเวนต์) ที่ผู้ใช้ทำกับเลย์เอาต์ ในบทที่ 4 “ส่วนการ<br />

ติดต่อกับผู้ใช้งาน (User Interface)” ได้กล่าวถึงการสร้างเลย์เอาต์เพื่อแสดงบนจอด้วยการใช้<br />

ออบเจ็กต์ View ไปแล้ว มาในบทนี้เราจะกล่าวถึงการจัดการเหตุการณ์ต่างๆ ที่เกิดขึ้นบนเลย์เอาต์ที่เรา<br />

ได้สร้างขึ้นบ้าง ไม่ว่าจะเป็นการกดปุ่มต่างๆ บนจอ, การกดปุ่มบนแป้นพิมพ์, การสัมผัสที่ตำแหน่งต่างๆ<br />

บนจอ รวมถึงการใช้งานไลบรารีขั้นสูงเกี่ยวกับ UI, การใช้เจสเจอร์ และการแสดงผลภาพแบบ 3 มิติ<br />

ด้วย<br />

การสร้างและการตรวจจับอีเวนต์<br />

การโต้ตอบระหว่างอุปกรณ์แอนดรอยด์และผู้ใช้งานนั้นจะถูกตรวจจับการกระทำนั้นโดยระบบ<br />

ปฏิบัติการ ซึ่งจะทำงานประสานกับเมธอดต่างๆ ของเฟรมเวิร์ค ถ้ามีการกดปุ่ม Back ที่อุปกรณ์<br />

เมธอด onBackPressed() ก็จะถูกเรียกใช้งาน เราสามารถตรวจจับอีเวนต์เหล่านี้ได้ด้วยการสร้าง<br />

อินสแตนซ์ของคลาส และทำการโอเวอร์ไรด์เมธอดเหล่านี้ โดยจะเรียกขั้นตอนนี้ว่า Event Handler<br />

การโต้ตอบกับผู้ใช้งานด้วยออบเจ็กต์ View หรือ ViewGroup นั้น ออบเจ็กต์เหล่านี้ก็รองรับการ<br />

ทำงานแบบ Event Listener ด้วยเช่นกัน โดยเมธอดพวกนี้จะคอยสังเกตเหตุการณ์ต่างๆ ที่แอพได้<br />

ประกาศไว้ เมื่อมีเหตุการณ์หรืออีเวนต์ตรงกัน ก็จะเริ่มทำงาน ยกตัวอย่างเช่น setOnClick<br />

Listener() จะคอยตรวจจับอีเวนต์ของการกดปุ่ม ซึ่งหลังจากการกดปุ่มก็จะเกิดการเรียกใช้งาน<br />

เมธอด onClick() เป็นต้น<br />

ออบเจ็กต์ประเภท Event Listener จะเรียกใช้เมธอดก็ต่อเมื่อมีอีเวนต์เกิดขึ้นเท่านั้น เพื่อหลีก<br />

เลี่ยงการใช้เมธอดโดยไม่จำเป็น อันจะทำให้ประสิทธิภาพของระบบต่ำลง สำหรับในบทนี้เราจะกล่าวถึง<br />

การใช้งาน Event Listener และ Event Handler ร่วมกับการกดปุ่มต่างๆ บนอุปกรณ์แอนดรอยด์<br />

รวมทั้งการสัมผัสจอภาพด้วย<br />

กรรมวิธี: การขัดจังหวะการทำงานโดยการกดปุ่มแบบฮาร์ดแวร์<br />

อุปกรณ์แอนดรอยด์มาตรฐานจะประกอบไปด้วยปุ่มแบบฮาร์ดแวร์หลายปุ่ม ซึ่งปุ่มเหล่านี้<br />

สามารถสร้างอีเวนต์ ตามที่แสดงในตารางที่ 5.1 ได้


118 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

ตารางที่ 5.1 ปุ่มต่างๆ ที่มีให้ใช้งานในอุปกรณ์แอนดรอยด์<br />

ชื่อปุ่ม อีเวนต์ที่เกิด รายละเอียด<br />

Power KEYCODE_POWER ใช้เปิด-ปิดอุปกรณ์ หรือเริ่มทำางานจากโหมด Sleep<br />

Back KEYCODE_BACK ปุ่มที่พากลับไปที่หน้าจอก่อนหน้านี้<br />

MENU KEYCODE_MENU ใช้แสดงเมนูของแอพที่ใช้ในขณะนั้น<br />

HOME KEYCODE_HOME ปุ่มกลับไปที่หน้า Home<br />

SEARCH KEYCODE_SEARCH ใช้แสดงเมนูค้นหาของแอพที่ใช้ในขณะนั้น<br />

Camera KEYCODE_CAMERA เปิดใช้กล้องถ่ายภาพ<br />

Volume KEYCODE_VOLUME_UP ควบคุมเสียงต่างๆ เช่น เสียงเรียกเข้า<br />

KEYCODE_VOLUME_DOWN<br />

เสียงสนทนาโทรศัพท์<br />

DPAD KEYCODE_DPAD_CENTER ทิศทางของปุ่มควบคุมทิศทาง<br />

KEYCODE_DPAD_UP<br />

KEYCODE_DPAD_DOWN<br />

KEYCODE_DPAD_LEFT<br />

KEYCODE_DPAD_RIGHT<br />

แทร็คบอล - ทิศทางของลูกกลิ้งหรือจอยสติ๊ก<br />

แป้นพิมพ์<br />

แป้นพิมพ์แบบฮาร์ดแวร์<br />

KEYCODE_0, …, KEYCODE_9,<br />

KEYCODE_A, KEYCODE_Z<br />

Media KEYCODE_HEADSETHOOK ปุ่มเล่น/หยุดบนหูฟัง<br />

ระบบจะส่งอีเวนต์ของการกดคีย์ไปยังเมธอดที่เกี่ยวข้องในวิว หรือแอคทิวิตี้ที่แสดงหรือทำงาน<br />

อยู่ในขณะนั้น ซึ่งเมธอดที่เรียกใช้มีดังนี้<br />

m onKeyUp(), onKeyDown(), onKeyLongPress() - เรียกใช้เมื่อมีการกดปุ่มบน<br />

อุปกรณ์<br />

m onTrackballEvent(), onTouchEvent() - เรียกใช้เมื่อมีการใช้งานแทร็คบอลหรือ<br />

การสัมผัสจอภาพ<br />

m onFocusChanged() - เรียกใช้เมื่อมีการเลือกหรือเปลี่ยนการโฟกัสวิว<br />

เมธอดเหล่านี้สามารถโอเวอร์ไรด์โดยแอ็กชั่นต่างๆ ของแอพ ยกตัวอย่างเช่น การปิดการทำงาน<br />

ของปุ่มกล้องถ่ายรูป (เพื่อป้องกันการเปิดใช้งานโดยไม่ได้ตั้งใจ) ซึ่งใช้อีเวนต์ onKeyDown() ในการ<br />

ทำงาน เราจะทำการขวางการทำงานของปุ่มนี้ด้วยการขัดจังหวะเมธอด KeyEvent.KEYCODE_CAMERA<br />

และส่งค่ากลับแป็น true


การสร้างและการตรวจจับอีเวนต์<br />

public boolean onKeyDown(int keyCode, KeyEvent event) {<br />

if (keyCode == KeyEvent.KEYCODE_CAMERA) {<br />

return true; // consume event, hence do nothing on camera button<br />

}<br />

return super.onKeyDown(keyCode, event);<br />

}<br />

แต่การใช้งานอีเวนต์ประเภทนี้ มีข้อยกเว้น คือ<br />

m การกดปุ่ม Power และปุ่ม Home จะถูกขัดจังหวะการทำงานโดยระบบปฏิบัติการ<br />

โดยตรง เราไม่สามารถเขียนคำสั่งเพื่อแก้ไขการทำงานนี้ได้<br />

m การกดปุ่ม BACK, ปุ่ม MENU, ปุ่ม HOME และปุ่ม SEARCH จะไม่ถูกขัดจังหวะการ<br />

ทำงานโดยระบบปฏิบัติการโดยตรง เนื่องจากในระบบปฏิบัติการแอนดรอยด์ 2.0<br />

ปุ่มดังกล่าวอาจเป็นได้ทั้งแบบฮาร์ดแวร์หรือซอฟต์แวร์<br />

ชุดคำสั่งที่ 5.1 จะแสดงตัวอย่างของการใช้งานปุ่มต่างๆ ดังนี้<br />

m ปุ่มกล้องถ่ายรูปและปุ่มซ้ายของ DPAD - เราจะใช้อีเวนต์ onKeyDown() เพื่อแสดง<br />

ข้อความบนจอ และให้ส่งค่ากลับว่า true เมื่ออีเวนต์นี้ทำงานเสร็จเรียบร้อย<br />

m ปุ่มเพิ่มเสียง (Volume Up) - เมื่อกดแล้วจะสั่งให้แสดงข้อความบนจอ<br />

m ปุ่ม SEARCH - เราจะใช้อีเวนต์ onKeyDown() และ startTracking() เพื่อแสดง<br />

ข้อความบนจอ<br />

m ปุ่ม Back - เมื่อกดแล้วจะเกิดอีเวนต์ onBackPressed()<br />

119<br />

คู่มือประกอบการพัฒนาแอพบนแอนดรอยด์เวอร์ชั่นแรกๆ นั้นได้อธิบายรายละเอียดเกี่ยวกับปุ่ม<br />

BACK ว่าโดยปกติแล้วการทำงานของปุ่มนี้เราจะไม่สามารถปรับแต่งอะไรได้ แต่อย่างไรก็ดี ถ้าเรา<br />

พัฒนาแอพบนแอนดรอย์ที่มีระดับ API Level 5 (Eclair) ขึ้นไป ก็จะมีคำสั่งสำหรับทำงานร่วมกับปุ่ม<br />

BACK คือ onBackPressed()<br />

การเขียนชุดคำสั่งเพื่อให้สามารถทำงานกับระบบปฏิบัติการที่มีระดับ API LEVEL น้อยกว่า 5<br />

นั้น เราจะใช้คำสั่ง KeyEvent.KEYCODE_BACK และยังคงใช้เมธอด onBackPressed() ได้เช่นกัน<br />

ตามที่แสดงในชุดคำสั่งที่ 5.1 (ชุดคำสั่งนี้สามารถคอมไพล์บนระบบปฏิบัติการแอนดรอยด์เวอร์ชั่น 2.0<br />

ขึ้นไปเท่านั้น แต่แอพที่ได้จากการคอมไพล์นี้จะยังคงทำงานกับระบบปฏิบัติการแอนดรอยด์เวอร์ชั่น<br />

ก่อนๆ ได้) การที่จะทำงานกับปุ่ม BACK นั้น เราจะต้องใช้เมธอด startTracking() เช่นเดียวกับ<br />

การใช้งานปุ่ม SEARCH ในชุดคำสั่งที่ 5.1<br />

ชุดคำสั่งที่ 5.1 src/com/cookbook/PhysicalKeyPress.java<br />

package com.cookbook.physkey;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.KeyEvent;<br />

import android.widget.Toast;


120 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

public class PhysicalKeyPress extends Activity {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

}<br />

public boolean onKeyDown(int keyCode, KeyEvent event) {<br />

switch (keyCode) {<br />

case KeyEvent.KEYCODE_CAMERA:<br />

Toast.makeText(this, "Pressed Camera Button",<br />

Toast.LENGTH_LONG).show();<br />

return true;<br />

case KeyEvent.KEYCODE_DPAD_LEFT:<br />

Toast.makeText(this, "Pressed DPAD Left Button",<br />

Toast.LENGTH_LONG).show();<br />

return true;<br />

case KeyEvent.KEYCODE_VOLUME_UP:<br />

Toast.makeText(this, "Pressed Volume Up Button",<br />

Toast.LENGTH_LONG).show();<br />

return false;<br />

case KeyEvent.KEYCODE_SEARCH:<br />

//example of tracking through to the KeyUp<br />

if(event.getRepeatCount() == 0)<br />

event.startTracking();<br />

return true;<br />

case KeyEvent.KEYCODE_BACK:<br />

// Make new onBackPressed compatible with earlier SDK's<br />

if (android.os.Build.VERSION.SDK_INT<br />

< android.os.Build.VERSION_CODES.ECLAIR<br />

&& event.getRepeatCount() == 0) {<br />

onBackPressed();<br />

}<br />

}<br />

return super.onKeyDown(keyCode, event);<br />

}<br />

public void onBackPressed() {<br />

Toast.makeText(this, "Pressed BACK Key",<br />

Toast.LENGTH_LONG).show();<br />

}<br />

public boolean onKeyUp(int keyCode, KeyEvent event) {<br />

if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking()<br />

&& !event.isCanceled()) {<br />

Toast.makeText(this, "Pressed SEARCH Key",<br />

Toast.LENGTH_LONG).show();<br />

return true;


การสร้างและการตรวจจับอีเวนต์<br />

121<br />

}<br />

}<br />

}<br />

return super.onKeyUp(keyCode, event);<br />

กรรมวิธี: การสร้างเมนู<br />

ในระบบปฏิบัติการแอนดรอยด์ เราสามารถสร้างเมนูได้ 3 แบบ ดังนี้<br />

m เมนูแบบ Options – เป็นเมนูหลักของแอพ ซึ่งจะแสดงเมื่อกดปุ่ม MENU เมนูที่แสดง<br />

นั้นจะประกอบไปด้วยไอคอน และเมื่อกดที่เมนู More เมนูอื่นๆ นอกเหนือจากนั้นก็จะ<br />

แสดงขึ้นมา<br />

m เมนูแบบ Context – เป็นรายการของเมนูที่จะแสดงแบบลอยซ้อนขึ้นมา (Floating)<br />

เมื่อมีการสัมผัสค้างที่จอภาพ<br />

m เมนูแบบ Submenu -- เป็นรายการของเมนูย่อยที่จะแสดงแบบลอยซ้อนขึ้นมา<br />

(Floating) เมื่อมีการเลือกที่เมนูแบบ Context<br />

เมนูแบบ Options จะสร้างขึ้นในขณะที่เรากดปุ่ม MENU ตอนที่แอพกำลังทำงาน ซึ่งการกด<br />

ปุ่มนี้จะเกิดการเรียกใช้เมธอด onCreateOptionsMenu() โดยภายในเมธอดนี้จะมีคำสั่งที่ทำงาน<br />

เกี่ยวกับเมนูแบบนี้<br />

menu.add(GROUP_DEFAULT, MENU_ADD, 0, “Add”)<br />

.setIcon(R.drawable.icon);<br />

ค่าตัวแรกที่จะต้องกำหนดให้แก่เมธอด add() ก็คือกลุ่มของรายการเมนู ค่าตัวที่ 2 ก็คือเลข<br />

ID ของรายการเมนูที่จะสร้าง ซึ่งค่านี้จะใช้อ้างอิงเมื่อเลือกรายการเมนูใดๆ ส่วนค่าตัวที่ 3 จะเป็นค่า<br />

ของลำดับการเรียงของรายการเมนู ถ้าค่านี้ไม่มีการกำหนดไว้ ระบก็จะใช้ลำดับตามรายการที่เพิ่มใน<br />

คำสั่ง และค่าตัวสุดท้ายคือข้อความที่จะใช้แสดงในรายการเมนู ซึ่งค่านี้อาจเป็นข้อความโดยตรงหรือ<br />

เป็นข้อความที่มาจากไฟล์รีซอร์สก็ได้ และเมนูแบบ Options จะเป็นเมนูเพียงชนิดเดียวที่เราสามารถ<br />

เพิ่มไอคอนลงไปในรายการเมนูได้ด้วยการใช้เมธอด setIcon()<br />

คำสั่งการสร้างเมนูนี้จะทำงานในตอนครั้งแรกที่แอพถูกเรียกใช้ และมีการกดปุ่ม MENU เพื่อ<br />

แสดงรายการเมนู ซึ่งหลังจากนั้นเราไม่จำเป็นต้องใช้คำสั่งสร้างเมนูเหล่านี้อีก เนื่องจากรายการเมนูที่<br />

เราสร้างไว้จะยังคงอยู่ตลอดระยะเวลาที่แอพกำลังทำงาน แต่เรายังคงสามารถใช้คำสั่ง onPrepare-<br />

OptionsMenu() เพื่อแก้ไขรายการเมนูในภายหลังระหว่างที่แอพกำลังทำงานได้เช่นกัน<br />

เมื่อมีการเลือกในรายการเมนูแบบ Options ก็จะมีการเรียกใช้เมธอด onOptionsItemSelected()<br />

ซึ่งเมธอดนี้จะส่งค่าเลข ID ของเมนูที่ถูกเลือก และนำเลข ID ไปทำงานต่อร่วมกับคำสั่ง<br />

เงื่อนไขเพื่อตรวจสอบว่าเลือกรายการใด<br />

สำหรับในส่วนนี้ เราจะเพิ่มรายการเมนูอันได้แก่ เพิ่มเอกสาร, ลบเอกสาร และส่งเอกสาร<br />

ซึ่งเราจะใช้รายการเมนูเหล่านี้มายกตัวอย่างให้เห็นการเพิ่มค่าและลดค่าตัวเลขใดๆ ด้วยการเลือกเมนู<br />

และนำค่าตัวเลขในขณะนั้นมาแสดงบนจอภาพเพื่อให้เห็นสถานะของการเลือกเมนูต่างๆ


122 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

ตัวอย่างจะแสดงรายการเมนู ซึ่งรายการลบเอกสารนั้น เราจะเปิดการใช้งานเมนูนี้ก็ต่อเมื่อมี<br />

การเพิ่มเอกสารมาแล้วก่อนหน้านี้ โดยเราจะจัดกลุ่มของรายการลบเอกสาร และซ่อนมันไว้เมื่อรายการ<br />

เอกสารในขณะนั้นมีค่าเท่ากับ 0 การทำงานดังกล่าวได้แสดงไว้แล้วในชุดคำสั่งที่ 5.2<br />

ชุดคำสั่งที่ 5.2 src/com/cookbook/building_menus/BuildingMenus.java<br />

package com.cookbook.building_menus;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.ContextMenu;<br />

import android.view.Menu;<br />

import android.view.MenuItem;<br />

import android.view.SubMenu;<br />

import android.view.View;<br />

import android.view.ContextMenu.ContextMenuInfo;<br />

import android.widget.TextView;<br />

import android.widget.Toast;<br />

public class BuildingMenus extends Activity {<br />

private final int MENU_ADD=1, MENU_SEND=2, MENU_DEL=3;<br />

private final int GROUP_DEFAULT=0, GROUP_DEL=1;<br />

private final int ID_DEFAULT=0;<br />

private final int ID_TEXT1=1, ID_TEXT2=2, ID_TEXT3=3;<br />

private String[] choices = {"Press Me", "Try Again", "Change Me"};<br />

private static int itemNum=0;<br />

private static TextView bv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

bv = (TextView) findViewById(R.id.focus_text);<br />

}<br />

registerForContextMenu((View) findViewById(R.id.focus_text));<br />

@Override<br />

public boolean onCreateOptionsMenu(Menu menu) {<br />

menu.add(GROUP_DEFAULT, MENU_ADD, 0, "Add")<br />

.setIcon(R.drawable.icon); //example of adding icon<br />

menu.add(GROUP_DEFAULT, MENU_SEND, 0, "Send");<br />

menu.add(GROUP_DEL, MENU_DEL, 0, "Delete");<br />

}<br />

return super.onCreateOptionsMenu(menu);


@Override<br />

public boolean onPrepareOptionsMenu(Menu menu) {<br />

if(itemNum>0) {<br />

menu.setGroupVisible(GROUP_DEL, true);<br />

} else {<br />

menu.setGroupVisible(GROUP_DEL, false);<br />

}<br />

return super.onPrepareOptionsMenu(menu);<br />

}<br />

การสร้างและการตรวจจับอีเวนต์<br />

123<br />

@Override<br />

public boolean onOptionsItemSelected(MenuItem item) {<br />

switch(item.getItemId()) {<br />

case MENU_ADD:<br />

create_note();<br />

return true;<br />

case MENU_SEND:<br />

send_note();<br />

return true;<br />

case MENU_DEL:<br />

delete_note();<br />

return true;<br />

}<br />

return super.onOptionsItemSelected(item);<br />

}<br />

@Override<br />

public void onCreateContextMenu(ContextMenu menu, View v,<br />

ContextMenuInfo menuInfo) {<br />

super.onCreateContextMenu(menu, v, menuInfo);<br />

if(v.getId() == R.id.focus_text) {<br />

SubMenu textMenu = menu.addSubMenu("Change Text");<br />

textMenu.add(0, ID_TEXT1, 0, choices[0]);<br />

textMenu.add(0, ID_TEXT2, 0, choices[1]);<br />

textMenu.add(0, ID_TEXT3, 0, choices[2]);<br />

menu.add(0, ID_DEFAULT, 0, "Original Text");<br />

}<br />

}<br />

@Override<br />

public boolean onContextItemSelected(MenuItem item) {<br />

switch(item.getItemId()) {<br />

case ID_DEFAULT:<br />

bv.setText(R.string.hello);<br />

return true;<br />

case ID_TEXT1:<br />

case ID_TEXT2:<br />

case ID_TEXT3:


124 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

}<br />

bv.setText(choices[item.getItemId()-1]);<br />

return true;<br />

}<br />

return super.onContextItemSelected(item);<br />

}<br />

void create_note() { // mock code to create note<br />

itemNum++;<br />

}<br />

void send_note() { // mock code to send note<br />

Toast.makeText(this, "Item: "+itemNum,<br />

Toast.LENGTH_SHORT).show();<br />

}<br />

void delete_note() { // mock code to delete note<br />

itemNum—;<br />

}<br />

แอคทิวิตี้ที่แสดงในชุดคำสั่งที่ 5.2 จะแสดงตัวอย่างของการสร้างเมนูแบบ Context และเมนู<br />

ย่อย ซึ่งเราได้เพิ่มวิวชื่อ focus_text ลงไปในเลย์เอาต์ตามชุดคำสั่งที่ 5.3 และประกาศการสร้างเมนู<br />

แบบ Context ด้วยฟังก์ชั่น registerForContextMenu() ซึ่งอยู่ในเมธอด onCreate()<br />

เมื่อมีการสัมผัสบนจอก็จะเกิดการเรียกใช้เมธอด onCreateContextMenu() เพื่อสร้างรายการ<br />

เมนูหลัก และใช้เมธอด addSubMenu() เพื่อสร้างเมนูย่อย ซึ่งเมนูย่อยนี้จะทำงานร่วมกับเมนูหลัก<br />

และเมธอด onContextItemSelected() จะทำงานเมื่อมีการเลือกรายการเมนู สำหรับด้านล่างนี้จะ<br />

แสดงถึงการใช้ข้อความเพื่อแสดงในรายการเมนู<br />

ชุดคำสั่งที่ 5.3 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

รูปที่ 5.1 และ 5.2 จะแสดงหน้าตาของเมนู เมื่อแสดงในรูปแบบต่างๆ


การสร้างและการตรวจจับอีเวนต์<br />

125<br />

รูปที่ 5.1 รูปบนแสดงตัวอย่างของเมนูแบบ Options และรูปล่าง<br />

แสดงถึงการเพิ่มเมนูในระหว่างที่แอพกำาลังทำางาน<br />

รูปที่ 5.2 รูปซ้ายแสดงตัวอย่างของเมนูแบบ Context เมื่อมีการสัมผัสค้างที่ส่วนของข้อความ<br />

และรูปขวาแสดงเมนู Change Text จะแสดงเมนูย่อย ซึ่งมีรายการเมนูย่อยจำานวน 3 ตัว


126 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

กรรมวิธี: การสร้างเมนูด้วย XML<br />

เราสามารถสร้างรายการเมนูจากข้อมูลในไฟล์ XML ได้ด้วยการนำชุดคำสั่งในกรรมวิธีก่อนหน้า<br />

นี้มาดัดแปลง ซึ่งวิธีการนี้จะเหมาะสมกับการสร้างรายการเมนูที่ซับซ้อนและมีขนาดใหญ่ และเรา<br />

สามารถใช้คำสั่งจาวาในการควบคุมการทำงานของเมนูได้<br />

ไฟล์ที่เก็บรายการเมนูนั้น เราจะใส่ไว้ในไดเร็กทอรี res/menu/ ตัวอย่างเช่น ถ้าต้องการสร้าง<br />

เมนูแบบ Context จากในหัวข้อก่อนหน้านี้ ก็ให้เขียนโครงสร้างของเมนูไว้ในไฟล์ XML ดังในชุดคำสั่ง<br />

ที่ 5.4 ดังนี้<br />

ชุดคำสั่งที่ 5.4 res/menu/context_menu.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

ในไฟล์ XML ด้านบนนี้ เราได้กำหนดรายการเมนูและเลข ID ของเมนูแต่ละตัวเอาไว้ ฉะนั้น<br />

เราจะแก้ไขเมธอด 2 ตัวที่ใช้ในชุดคำสั่งที่ 5.2 ให้เป็นคำสั่งใหม่ตามที่แสดงในชุดคำสั่งที่ 5.5 ดังนี้<br />

ชุดคำสั่งที่ 5.5 การเปลี่ยนเมธอดในแอคทิวิตี้หลัก<br />

@Override<br />

public void onCreateContextMenu(ContextMenu menu, View v,<br />

ContextMenuInfo menuInfo) {<br />

super.onCreateContextMenu(menu, v, menuInfo);<br />

MenuInflater inflater = getMenuInflater();<br />

inflater.inflate(R.menu.context_menu, menu);<br />

}<br />

@Override<br />

public boolean onContextItemSelected(MenuItem item) {<br />

switch(item.getItemId()) {<br />

case R.id.orig:<br />

bv.setText(R.string.hello);<br />

return true;<br />

case R.id.text1:<br />

bv.setText(choices[0]);


}<br />

return true;<br />

case R.id.text2:<br />

bv.setText(choices[1]);<br />

return true;<br />

case R.id.text3:<br />

bv.setText(choices[2]);<br />

return true;<br />

}<br />

return super.onContextItemSelected(item);<br />

การสร้างและการตรวจจับอีเวนต์<br />

127<br />

กรรมวิธี: การใช้งานปุ่มค้นหา<br />

ถ้าแอพที่กำลังใช้งานอยู่นั้นรองรับการค้นหาข้อมูลภายในแอพละก็ เมื่อกดปุ่ม SEARCH แอพก็<br />

จะแสดงเมนูที่เกี่ยวข้องกับการค้นหาข้อมูลขึ้นมา ในอุปกรณ์แอนดรอยด์ที่ไม่มีปุ่ม SEARCH แบบ<br />

ฮาร์ดแวร์ เราก็สามารถสร้างเมนู SEARCH ขึ้นมาเองได้โดยกำหนดให้เรียกเมธอดชื่อ onSearchRequested()<br />

การทำงานของแอพที่รองรับการค้นหานั้น เมื่อกดปุ่ม SEARCH แล้ว การทำงานในส่วนนี้จะถูก<br />

กำหนดให้มีลำดับความสำคัญสูงสุด (SingleTop) เพื่อให้แอพสามารถค้นหาข้อมูลในขณะที่แอคทิวิตี้<br />

อื่นๆ กำลังทำงานอยู่ได้ โดยเราจะกำหนดการทำงานลงในไฟล์ Manifest ของแอพดังนี้<br />

<br />

<br />

<br />

<br />

<br />

<br />

ในชุดคำสั่งที่ 5.6 จะแสดงการประกาศแอคทิวิตี้<br />

ชุดคำสั่งที่ 5.6 res/xml/my_search.xml<br />

<br />

<br />

<br />

ในส่วนนี้จะแสดงการใช้งานในส่วนของการค้นหา เมื่อแอพเริ่มทำงาน แอคทิวิตี้ตามที่แสดงใน<br />

ชุดคำสั่งที่ 5.7 ก็จะทำงานร่วมกับไฟล์ main.xml<br />

ชุดคำสั่งที่ 5.7 src/com/cookbook/search_diag/MainActivity.java<br />

package com.cookbook.search_diag;<br />

import android.app.Activity;


128 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

import android.os.Bundle;<br />

public class MainActivity extends Activity {<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

}<br />

}<br />

ถ้ามีการกดปุ่ม SEARCH แอคทิวิตี้ที่เกี่ยวข้องกับการค้นหาก็จะเริ่มทำงาน เมธอด onCreate()<br />

จะตรวจสอบการทำงานของอินเทนต์ ACTION_SEARCH โดยในชุดคำสั่งที่ 5.8 จะแสดงการทำงานของ<br />

แอคทิวิตี้หลักซึ่งจะแสดงผลการค้นหาบนจอ<br />

ชุดคำสั่งที่ 5.8 src/com/cookbook/search_diag/SearchDialogExample.java<br />

package com.cookbook.search_diag;<br />

import android.app.Activity;<br />

import android.app.SearchManager;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.widget.Toast;<br />

public class SearchDialogExample extends Activity {<br />

/** Called when the activity is first created. */<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Intent intent = getIntent();<br />

}<br />

}<br />

if (Intent.ACTION_SEARCH.equals(intent.getAction())) {<br />

String query = intent.getStringExtra(SearchManager.QUERY);<br />

Toast.makeText(this, "The QUERY: " + query,<br />

Toast.LENGTH_LONG).show();<br />

}<br />

กรรมวิธี: การตอบสนองต่อการสัมผัสจอภาพ<br />

การกระทำใดๆ ที่ทำกับจอภาพ ไม่ว่าจะเป็นการสัมผัสจอภาพหรือการใช้แทร็คบอลเพื่อเลือกจุด<br />

ต่างๆ บนจอภาพนั้น การทำงานทั้งหมดจะเป็นการโต้ตอบกันระหว่างวิวที่กำหนดไว้บนจอแต่ละจุด<br />

เพราะวิวก็ถือเป็นส่วนหนึ่งของเลย์เอาต์ของจอภาพ ระบบปฏิบัติการจะจัดลำดับการทำงานกับจอภาพ<br />

โดยจะส่งผ่านอีเวนต์ไปยังวิวที่วางไว้แต่ละจุดเพื่อสั่งให้แอพทำงานตามที่กำหนดไว้


การสร้างและการตรวจจับอีเวนต์<br />

129<br />

ชุดคำสั่งที่ 5.9 จะแสดงปุ่มชื่อ ex_button ซึ่งคอยตรวจจับอีเวนต์ของการสัมผัสและสัมผัส<br />

ค้างด้วย Event Listener ที่สร้างไว้ เมื่อมีอีเวนต์เกิดขึ้นตรงกับที่กำหนดไว้ก็จะเรียกเมธอดที่เกี่ยวข้อง<br />

ขึ้นมาทำงาน และแสดง Toast บนจอภาพเพื่อแจ้งว่าเมธอดได้ถูกเรียกให้ทำงานแล้ว<br />

ชุดคำสั่งที่ 5.9 src/com/cookbook/touch_examples/TouchExamples.java<br />

package com.cookbook.touch_examples;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.view.View.OnLongClickListener;<br />

import android.widget.Button;<br />

import android.widget.Toast;<br />

public class TouchExamples extends Activity {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Button ex = (Button) findViewById(R.id.ex_button);<br />

}<br />

}<br />

ex.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

Toast.makeText(TouchExamples.this, "Click",<br />

Toast.LENGTH_SHORT).show();<br />

}<br />

});<br />

ex.setOnLongClickListener(new OnLongClickListener() {<br />

public boolean onLongClick(View v) {<br />

Toast.makeText(TouchExamples.this, "LONG Click",<br />

Toast.LENGTH_SHORT).show();<br />

return true;<br />

}<br />

});<br />

ในชุดคำสั่งที่ 5.10 จะแสดงเลย์เอาต์ที่สร้างปุ่มกดเอาไว้<br />

ชุดคำสั่งที่ 5.10 res/layout/main.xml<br />

<br />


130 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

android:orientation="vertical"<br />

android:layout_width="fill_parent"<br />

android:layout_height="fill_parent"><br />

<br />

<br />

เพื่อให้ชุดคำสั่งมีความกระชับมากขึ้น คำสั่งที่ใช้ในชุดคำสั่งที่ 5.9 สามารถเขียนใหม่เพื่อให้สั้น<br />

เข้าใจง่าย และสามารถนำกลับมาใช้งานใหม่ได้ง่าย ดังนี้<br />

View.OnClickListener myTouchMethod = new View.OnClickListener() {<br />

public void onClick(View v) {<br />

//insert relevant action here<br />

}<br />

};<br />

ex.setOnClickListener(myTouchMethod);<br />

และวิธีการเขียนคำสั่งอื่นๆ นอกเหนือจากนี้ก็คือให้แอคทิวิตี้ใช้งานอินเตอร์เฟซ onClickListener()<br />

ซึ่งเมธอดจะทำงานในระดับเดียวกับแอคทิวิตี้เพื่อหลีกเลี่ยงการใช้งานคลาสเพิ่มเติม<br />

public class TouchExamples extends Activity implements OnClickListener {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Button ex = (Button) findViewById(R.id.ex_button);<br />

ex.setOnClickListener(this);<br />

}<br />

}<br />

public void onClick(View v) {<br />

if(v.getId() == R.id.directory_button) {<br />

// insert relevant action here<br />

}<br />

}<br />

คำสั่งข้างต้นจะใช้งานเมธอด onClick() ซึ่งทำงานในระดับแอคทิวิตี้เพื่อแสดงขั้นตอนว่าวิว<br />

สามารถจัดการอีเวนต์ที่เกิดจากการสัมผัสได้อย่างไร<br />

กรรมวิธี: การตรวจจับอีเวนต์เจสเจอร์<br />

ตามที่ได้พูดถึงไปแล้วในตอนต้นของบทนี้ วิวแต่ละวิวจะมีอีเวนต์ onTouchEvent() คอยตรวจ<br />

จับเหตุการณ์การสัมผัสจอภาพ ซึ่งทำงานร่วมกับการตรวจจับการทำงานของเจสเจอร์ อันประกอบไป<br />

ด้วย OnGestureListener ดังนี้


m onDown() – เกิดขึ้นเมื่อมีการสัมผัสจอภาพ<br />

m onFling() – เกิดขึ้นเมื่อมีการสัมผัสจอภาพและลาก<br />

m onLongPress() – เกิดขึ้นเมื่อมีการสัมผัสจอภาพค้างไว้<br />

m onScroll() – เกิดขึ้นเมื่อมีการลากนิ้ว เพื่อเลื่อนออบเจ็กต์ต่างๆ<br />

m onShowPress() – เกิดขึ้นเมื่อมีการสัมผัสจอภาพ แต่ยังไม่มีการลากหรือปล่อยนิ้ว<br />

m onSingleTapUp() – เกิดขึ้นเมื่อมีการปล่อยนิ้ว<br />

131<br />

ถ้าต้องการตรวจจับอีเวนต์เพียงอีเวนต์เดียว ก็ให้ใช้คลาส SimpleOnGestureListener<br />

เพื่อสร้างการตรวจจับดังกล่าวขึ้นมา ซึ่งอีเวนต์ที่เหลือที่ไม่ได้ใช้งานจะส่งค่ากลับเป็น false<br />

อีเวนต์ onFling() จะประกอบด้วยเหตุการณ์ 2 อย่าง คือ การสัมผัสจอภาพ (MotionEvent<br />

อันแรก) และการปล่อยนิ้ว (MotionEvent อันที่ 2) โดยที่แต่ละการสัมผัสที่เกิดขึ้นกับจอนั้นจะส่งค่า<br />

กลับออกมาเป็นค่าคู่อันดับ (x,y) ซึ่ง x จะแทนแกนตำแหน่งแนวนอน และ y จะแทนแกนตำแหน่ง<br />

แนวตั้ง<br />

ชุดคำสั่งที่ 5.11 จะแสดงแอคทิวิตี้ที่ใช้เมธอด onFling() โดยจะตรวจจับเมื่อมีการเคลื่อนไหว<br />

ของการสัมผัสมากพอ (ในที่นี้กำหนดไว้ที่ 60 พิกเซล) และเมื่อเกิดเหตุการณ์ตรงกับที่กำหนดไว้ ก็จะ<br />

แสดงข้อความขึ้นบนจอ<br />

ชุดคำสั่งที่ 5.11 src/com/cookbook/fling_ex/FlingExample.java<br />

การสร้างและการตรวจจับอีเวนต์<br />

package com.cookbook.fling_ex;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.GestureDetector;<br />

import android.view.MotionEvent;<br />

import android.view.GestureDetector.SimpleOnGestureListener;<br />

import android.widget.TextView;<br />

public class FlingExample extends Activity {<br />

private static final int LARGE_MOVE = 60;<br />

private GestureDetector gestureDetector;<br />

TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.text_result);<br />

gestureDetector = new GestureDetector(this,<br />

new SimpleOnGestureListener() {<br />

@Override<br />

public boolean onFling(MotionEvent e1, MotionEvent e2,<br />

float velocityX, float velocityY) {


132 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

if (e1.getY() - e2.getY() > LARGE_MOVE) {<br />

tv.append("\nFling Up with velocity " + velocityY);<br />

return true;<br />

} else if (e2.getY() - e1.getY() > LARGE_MOVE) {<br />

tv.append("\nFling Down with velocity " + velocityY);<br />

return true;<br />

} else if (e1.getX() - e2.getX() > LARGE_MOVE) {<br />

tv.append("\nFling Left with velocity " + velocityX);<br />

return true;<br />

} else if (e2.getX() - e1.getX() > LARGE_MOVE) {<br />

tv.append("\nFling Right with velocity " + velocityX);<br />

return true;<br />

}<br />

}<br />

return false;<br />

} });<br />

}<br />

@Override<br />

public boolean onTouchEvent(MotionEvent event) {<br />

return gestureDetector.onTouchEvent(event);<br />

}<br />

จากชุดคำสั่งข้างต้น เราได้แสดงข้อความใน TextView โดยใช้ข้อความจากที่กำหนดไว้ในไฟล์<br />

เลย์เอาต์ XML ดังแสดงในชุดคำสั่งที่ 5.12<br />

ชุดคำสั่งที่ 5.12 res/layout/main.xml<br />

<br />

<br />

<br />


กรรมวิธี: การใช้งานมัลติทัช<br />

การสร้างและการตรวจจับอีเวนต์<br />

133<br />

อีเวนต์มัลติทัช เป็นอีเวนต์ที่เกิดจากการสัมผัสจอภาพ พร้อมกันมากกว่าหนึ่งจุด พร้อมๆ กัน<br />

ซึ่งเราสามารถตรวจจับเหตุการณ์นี้ได้ด้วยการใช้ OnTouchListener ที่จะคอยตรวจจับการกระทำบน<br />

จอในแบบต่างๆ ไม่ว่าจะเป็น:<br />

m ACTION_DOWN – เกิดขึ้นเมื่อมีการสัมผัสจอมากกว่าหนึ่งจุดโดยเริ่มการสัมผัสจุดที่ 1<br />

m ACTION_POINTER_DOWN – เกิดขึ้นเมื่อมีการสัมผัสจอจุดที่ 2<br />

m ACTION_MOVE – เกิดขึ้นเมื่อมีการเคลื่อนไหวหลังจากสัมผัสจอทั้ง 2 จุดแล้ว<br />

m ACTION_POINTER_UP – เกิดขึ้นเมื่อมีการปล่อยนิ้วที่สัมผัสจอจุดที่ 2<br />

m ACTION_UP – เกิดขึ้นเมื่อมีการปล่อยนิ้วที่สัมผัสจอภาพจุดที่ 1 และการทำงานของ<br />

เจสเจอร์เสร็จสิ้น<br />

ส่วนนี้จะเป็นการแสดงรูปภาพบนจอ และใช้มัลติทัชเพื่อย่อหรือขยายภาพ และสัมผัสที่ภาพเพื่อ<br />

ลากไปยังตำแหน่งต่างๆ บนจอได้ ในชุดคำสั่งที่ 5.13 จะแสดงแอคทิวิตี้ที่ใช้งาน OnTouchListener<br />

ซึ่งได้กำหนดไว้ในเมธอด onCreate() และเมื่อมีการสัมผัสจอภาพ เมธอด onTouch() ก็จะเริ่ม<br />

ตรวจสอบการเคลื่อนไหวที่เกิดขึ้นตามรายละเอียดดังนี้<br />

m ถ้ามีการสัมผัสที่จอด้วยนิ้วที่ 1 สถานะการสัมผัสจะแสดงค่าเป็นการลาก และตำแหน่งที่<br />

สัมผัสก็จะถูกเก็บไว้ในระบบ<br />

m ถ้ามีการสัมผัสที่จอภาพด้วยนิ้วที่ 2 ในขณะที่นิ้วที่หนึ่งยังคงสัมผัสอยู่ ระบบจะคำนวณ<br />

ระยะห่างระหว่างนิ้วทั้งสอง และถ้าระยะห่างมีค่ามากกว่าที่กำหนดไว้ (ในที่นี้กำหนดไว้ที่<br />

50 พิกเซล) สถานะการสัมผัสจะแสดงถึงการย่อขยายรูปภาพ ซึ่งระบบจะเก็บค่าระยะ<br />

ห่างและจุดกึ่งกลางของระยะห่างทั้ง 2 เอาไว้<br />

m ถ้ามีการเคลื่อนไหวของนิ้วที่สัมผัสอยู่ทั้ง 2 นิ้ว อาจเป็นนิ้วใดนิ้วหนึ่ง หรือทั้ง 2 นิ้ว<br />

พร้อมๆ กัน สถานะการสัมผัสจะแสดงค่าเป็นการทำงานแบบมัลติทัช<br />

m ถ้าปล่อยนิ้วทั้งหมดที่สัมผัสจอภาพ สถานะการสัมผัสก็จะแสดงค่าว่าไม่มีการเคลื่อนไหว<br />

ชุดคำสั่งที่ 5.13 src/com/cookbook/multitouch/MultiTouch.java<br />

package com.cookbook.multitouch;<br />

import android.app.Activity;<br />

import android.graphics.Matrix;<br />

import android.os.Bundle;<br />

import android.util.FloatMath;<br />

import android.view.MotionEvent;<br />

import android.view.View;<br />

import android.view.View.OnTouchListener;<br />

import android.widget.ImageView;


134 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

public class MultiTouch extends Activity implements OnTouchListener {<br />

// Matrix instances to move and zoom image<br />

Matrix matrix = new Matrix();<br />

Matrix eventMatrix = new Matrix();<br />

// possible touch states<br />

final static int NONE = 0;<br />

final static int DRAG = 1;<br />

final static int ZOOM = 2;<br />

int touchState = NONE;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

ImageView view = (ImageView) findViewById(R.id.imageView);<br />

view.setOnTouchListener(this);<br />

}<br />

final static float MIN_DIST = 50;<br />

static float eventDistance = 0;<br />

static float centerX =0, centerY = 0;<br />

@Override<br />

public boolean onTouch(View v, MotionEvent event) {<br />

ImageView view = (ImageView) v;<br />

switch (event.getAction() & MotionEvent.ACTION_MASK) {<br />

case MotionEvent.ACTION_DOWN:<br />

//primary touch event starts: remember touch down location<br />

touchState = DRAG;<br />

centerX = event.getX(0);<br />

centerY = event.getY(0);<br />

eventMatrix.set(matrix);<br />

break;<br />

case MotionEvent.ACTION_POINTER_DOWN:<br />

//secondary touch event starts: remember distance and center<br />

eventDistance = calcDistance(event);<br />

calcMidpoint(centerX, centerY, event);<br />

if (eventDistance > MIN_DIST) {<br />

eventMatrix.set(matrix);<br />

touchState = ZOOM;<br />

}<br />

break;


การสร้างและการตรวจจับอีเวนต์<br />

135<br />

case MotionEvent.ACTION_MOVE:<br />

if (touchState == DRAG) {<br />

//single finger drag, translate accordingly<br />

matrix.set(eventMatrix);<br />

matrix.setTranslate(event.getX(0) - centerX,<br />

event.getY(0) - centerY);<br />

} else if (touchState == ZOOM) {<br />

//multi-finger zoom, scale accordingly around center<br />

float dist = calcDistance(event);<br />

if (dist > MIN_DIST) {<br />

matrix.set(eventMatrix);<br />

float scale = dist / eventDistance;<br />

}<br />

}<br />

matrix.postScale(scale, scale, centerX, centerY);<br />

// Perform the transformation<br />

view.setImageMatrix(matrix);<br />

break;<br />

case MotionEvent.ACTION_UP:<br />

case MotionEvent.ACTION_POINTER_UP:<br />

touchState = NONE;<br />

break;<br />

}<br />

}<br />

return true;<br />

private float calcDistance(MotionEvent event) {<br />

float x = event.getX(0) - event.getX(1);<br />

float y = event.getY(0) - event.getY(1);<br />

return FloatMath.sqrt(x * x + y * y);<br />

}<br />

}<br />

private void calcMidpoint(float centerX, float centerY,<br />

MotionEvent event) {<br />

centerX = (event.getX(0) + event.getX(1))/2;<br />

centerY = (event.getY(0) + event.getY(1))/2;<br />

}


136 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

ชุดคำสั่งที่ 5.4 จะแสดงเลย์เอาต์ที่กำหนดรูปภาพที่จะใช้ย่อ ขยาย ซึ่งในตัวอย่างนี้จะใช้ไฟล์ชื่อ<br />

icon.png ที่มาพร้อมกับโปรแกรม Eclipse อยู่แล้ว โดยเราสามารถเปลี่ยนเป็นไฟล์ภาพอื่นตามที่<br />

ต้องการได้<br />

ชุดคำสั่งที่ 5.14 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

ไลบรารีขั้นสูงที่ใช้ในส่วนการติดต่อกับผู้ใช้งาน<br />

ในบางครั้งการสร้างหน้าจอในส่วนที่ติดต่อกับผู้ใช้งานที่มีคุณสมบัติการทำงานบางอย่างนั้น<br />

จำเป็นต้องใช้ชุดคำสั่งที่ซับซ้อน ซึ่งต้องปรับแต่งแอพให้สามารถทำงานได้อย่างมีประสิทธิภาพโดยไม่<br />

ทำให้การทำงานช้าลง ซึ่งการพัฒนาเองอาจจะยุ่งยากหรือมีประสิทธิภาพไม่ดีเท่าที่ควร ในระบบ<br />

ปฏิบัติการแอนดรอยด์มีไลบรารีที่น่าสนใจอยู่หลายตัวที่สามารถนำมาใช้ได้ สำหรับในหัวข้อนี้เราจะมาดู<br />

การนำไลบรารีที่มีอยู่มาเขียนเป็นชุดคำสั่งเพื่อให้ได้ผลลัพธ์ตามต้องการกัน<br />

กรรมวิธี: การใช้เจสเจอร์<br />

เจสเจอร์ (Gesture) คือการลากนิ้วเป็นรูปร่างต่างๆ บนจอภาพ ซึ่งไลบรารีแพ็คเกจชื่อ<br />

android.gesture จะตรวจสอบเหตุการณ์และจัดการทำงานเหล่านี้ ในชุดพัฒนาแอพบนระบบ<br />

ปฏิบัติการแอนดรอยด์นั้นจะมีตัวอย่างของการใช้ไลบรารีที่เกี่ยวกับเจสเจอร์ โดยอยู่ในไดเร็กทอรี<br />

platforms/android-2.0/samples/GestureBuilder/ ซึ่งนำเอาตัวสร้างเจสเจอร์นี้มาติดตั้งและ<br />

ทำงานในอุปกรณ์แอนดรอยด์ได้ นอกจากนั้นตัวสร้างเจสเจอร์นี้จะสร้างไฟล์เจสเจอร์และเก็บไว้ใน<br />

ไดเร็กทอรี /sdcard/gestures โดยที่เราสามารถคัดลอกไฟล์ดังกล่าวออกมาจากเครื่องและนำไปใช้<br />

เป็นไฟล์ประกอบการเขียนแอพในหัวข้อนี้ได้ด้วย<br />

รูปที่ 5.3 จะแสดงตัวอย่างการสร้างเจสเจอร์ของการวาดนิ้วเป็นตัวเลขต่างๆ เราสามารถสร้าง<br />

เจสเจอร์หลายๆ ตัว โดยใช้ชื่อเจสเจอร์เดียวกันได้ อย่างเช่นการวาดตัวเลข 1 ซึ่งวาดได้หลายแบบ<br />

เราก็จะเก็บรูปแบบการวาดเหล่านี้ไว้ในเจสเจอร์ที่เกี่ยวข้องกับเลข 1 ทำให้ระบบตรวจสอบรูปแบบของ<br />

การวาดเลข 1 ได้หลากหลายและแม่นยำยิ่งขึ้น<br />

หลังจากที่สร้างไฟล์เจสเจอร์ที่เก็บการวาดนิ้วเป็นรูปของตัวเลขตั้งแต่0 ถึง 9 แล้ว เราจะนำไฟล์นี้<br />

มาเก็บไว้ในไดเร็กทอรี res/raw/numbers ส่วนเลย์เอาต์ที่จะนำมาใช้งานร่วมกับเจสเจอร์นี้ได้แสดงไว้


ไลบรารีขั้นสูงที่ใช้ในส่วนการติดต่อกับผู้ใช้งาน<br />

137<br />

แล้วในชุดคำสั่งที่ 5.16 ซึ่งไลบรารีเจสเจอร์จะเริ่มทำงานร่วมกับไฟล์เจสเจอร์ที่เราได้สร้างไว้แล้ว<br />

ในส่วนนี้เราจะมาเพิ่ม GestureOverlayVew ตรงด้านบนสุดของจอภาพ และเรียกใช้ตัวตรวจ<br />

จับเหตุการณ์ชื่อ OnGesturePerformedListener เมื่อมีการวาดเจสเจอร์บนจอภาพ ค่าที่วาดไว้จะ<br />

ถูกส่งไปยังเมธอด onGesturePerformed() เพื่อตรวจดูว่าการวาดเจสเจอร์ที่ส่งเข้ามานั้นตรงกับ<br />

เจสเจอร์หรือใกล้เคียงกับเจสเจอร์ใดเพื่อที่จะได้แสดงค่าของตัวเลขได้ถูกต้อง<br />

การทำงานในหัวข้อนี้ เราจะตรวจจับเหตุการณ์ของการวาดนิ้วมือเพื่อนำไปตรวจสอบกับ<br />

เจสเจอร์ที่เราสร้างไว้ และทำการแสดงค่าคะแนนของการเปรียบเทียบว่าตรงกับเจสเจอร์ของตัวเลขใด<br />

มากที่สุด<br />

ชุดคำสั่งที่ 5.15 res/layout/main.xml<br />

รูปที่ 5.3 แสดงจอภาพของตัวสร้างเจสเจอร์และตัวอย่าง<br />

เจสเจอร์ที่สร้างไว้<br />

<br />

<br />


138 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

android:text="Draw a number"<br />

android:layout_margin="10dip"/><br />

<br />

<br />

<br />

การทำงานของชุดคำสั่งนี้ จะตรวจสอบเจสเจอร์ที่รับเข้ามาและทำนายว่าตรงกับเจสเจอร์ใด<br />

พร้อมทั้งแสดงผลลัพธ์บนจอภาพ ซึ่งการตรวจสอบเจสเจอร์นั้นสามารถตรวจสอบจากการวาดนิ้ว<br />

ทั้งหมดหรือบางส่วนก็ได้ ตามรูปที่ 5.4<br />

รูปที่ 5.4 แสดงตัวอย่างการตรวจสอบเจสเจอร์ จะเห็นว่าการวาดนิ้วตามในภาพนั้น<br />

จะใกล้เคียงกับเลข 5 มากที่สุด สังเกตได้จากคะแนนที่สูงที่สุด (five 12.60)


ไลบรารีขั้นสูงที่ใช้ในส่วนการติดต่อกับผู้ใช้งาน<br />

139<br />

ชุดคำสั่งที่ 5.16 src/com/cookbook/gestures/Gestures.java<br />

package com.cookbook.gestures;<br />

import java.text.DecimalFormat;<br />

import java.text.NumberFormat;<br />

import java.util.ArrayList;<br />

import android.app.Activity;<br />

import android.gesture.Gesture;<br />

import android.gesture.GestureLibraries;<br />

import android.gesture.GestureLibrary;<br />

import android.gesture.GestureOverlayView;<br />

import android.gesture.Prediction;<br />

import android.gesture.GestureOverlayView.OnGesturePerformedListener;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

public class Gestures extends Activity<br />

implements OnGesturePerformedListener {<br />

private GestureLibrary mLibrary;<br />

private TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.prediction);<br />

mLibrary = GestureLibraries.fromRawResource(this, R.raw.numbers);<br />

if (!mLibrary.load()) finish();<br />

}<br />

GestureOverlayView gestures =<br />

(GestureOverlayView) findViewById(R.id.gestures);<br />

gestures.addOnGesturePerformedListener(this);<br />

public void onGesturePerformed(GestureOverlayView overlay,<br />

Gesture gesture) {<br />

ArrayList predictions = mLibrary.recognize(gesture);<br />

String predList = "";<br />

NumberFormat formatter = new DecimalFormat("#0.00");<br />

for(int i=0; i


140 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

}<br />

}<br />

+ formatter.format(prediction.score) + "\n";<br />

}<br />

tv.setText(predList);<br />

กรรมวิธี: การวาดภาพแบบ 3 มิติ<br />

ระบบปฏิบัติการแอนดรอยด์รองรับการทำงานร่วมกับไลบรารี OpenGL ES (Open Graphic<br />

Library for Embedded Systems) ในหัวข้อนี้จะแสดงให้เห็นการสร้างรูปปิรามิดแบบ 3 มิติด้วยการ<br />

ใช้ไลบรารีดังกล่าว และสั่งให้เคลื่อนไหวไปรอบๆ จอ และหมุนรอบตัวเองด้วย แอคทิวิตี้หลักในที่นี้จะมี<br />

การเรียกใช้งานคลาสจำนวน 2 คลาส คือ คลาสตัวแรกจะใช้ในการสร้างรูปทรงปิรามิด (ชุดคำสั่งที่<br />

5.17) และคลาสตัวที่ 2 จะใช้ในการสร้างพื้นผิวของรูปทรง (ชุดคำสั่งที่ 5.18)<br />

ชุดคำสั่งที่ 5.17 src/com/cookbook/open_gl/Pyramid.java<br />

package com.cookbook.open_gl;<br />

import java.nio.ByteBuffer;<br />

import java.nio.ByteOrder;<br />

import java.nio.IntBuffer;<br />

import javax.microedition.khronos.opengles.GL10;<br />

class Pyramid {<br />

public Pyramid() {<br />

int one = 0x10000;<br />

/* square base and point top to make a pyramid */<br />

int vertices[] = {<br />

-one, -one, -one,<br />

-one, one, -one,<br />

one, one, -one,<br />

one, -one, -one,<br />

0, 0, one<br />

};<br />

/* purple fading to white at the top */<br />

int colors[] = {<br />

one, 0, one, one,<br />

one, 0, one, one,<br />

one, 0, one, one,<br />

one, 0, one, one,<br />

one, one, one, one<br />

};<br />

/* triangles of the vertices above to build the shape */<br />

byte indices[] = {


};<br />

0, 1, 2, 0, 2, 3, //square base<br />

0, 3, 4, // side 1<br />

0, 4, 1, // side 2<br />

1, 4, 2, // side 3<br />

2, 4, 3 // side 4<br />

ไลบรารีขั้นสูงที่ใช้ในส่วนการติดต่อกับผู้ใช้งาน<br />

141<br />

// Buffers to be passed to gl*Pointer() functions<br />

ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);<br />

vbb.order(ByteOrder.nativeOrder());<br />

mVertexBuffer = vbb.asIntBuffer();<br />

mVertexBuffer.put(vertices);<br />

mVertexBuffer.position(0);<br />

ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length*4);<br />

cbb.order(ByteOrder.nativeOrder());<br />

mColorBuffer = cbb.asIntBuffer();<br />

mColorBuffer.put(colors);<br />

mColorBuffer.position(0);<br />

}<br />

mIndexBuffer = ByteBuffer.allocateDirect(indices.length);<br />

mIndexBuffer.put(indices);<br />

mIndexBuffer.position(0);<br />

public void draw(GL10 gl) {<br />

gl.glFrontFace(GL10.GL_CW);<br />

gl.glVertexPointer(3, GL10.GL_FIXED, 0, mVertexBuffer);<br />

gl.glColorPointer(4, GL10.GL_FIXED, 0, mColorBuffer);<br />

gl.glDrawElements(GL10.GL_TRIANGLES, 18, GL10.GL_UNSIGNED_BYTE,<br />

mIndexBuffer);<br />

}<br />

}<br />

private IntBuffer mVertexBuffer;<br />

private IntBuffer mColorBuffer;<br />

private ByteBuffer mIndexBuffer;<br />

ปิรามิดจะประกอบด้วยมุมจำนวน 5 มุม โดย 4 มุมจะอยู่ที่ฐานจัดวางเป็นรูปสี่เหลี่ยมจัตุรัส และ<br />

อีก 1 มุมอยู่ตรงส่วนยอด เราจะอ้างอิงจุดศูนย์กลางของรูปทรงนี้ด้วยค่า (0,0,0)<br />

สีที่แสดงบนรูปทรงนั้นจะสัมพันธ์กับตำแหน่งของมุมของรูปทรง ณ ตอนนั้น เราจะกำหนดให้<br />

มุมตรงส่วนฐานมีสีม่วงและมุมตรงส่วนยอดมีสีขาว และใช้ไลบรารีในการไล่สีดังกล่าว ซึ่งการสร้าง<br />

รูปทรงและการไล่สีของรูปทรงแบบนี้จะทำให้ภาพที่แสดงผลมีลักษณะเป็น 3 มิติยิ่งขึ้น


142 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

เมธอด draw() จะถูกใช้ในการประกาศอีลีเมนต์ของรูปสามเหลี่ยมทั้ง 4 ด้าน ส่วนฐานที่เป็นรูป<br />

สี่เหลี่ยมจัตุรัสนั้น เราจะสร้างจากรูปสามเหลี่ยม 2 รูปต่อกัน ทำให้รูปทรงปิรามิดนี้ประกอบไปด้วยรูป<br />

สามเหลี่ยมจำนวน 6 รูป และประกอบไปด้วยมุมของรูปสามเหลี่ยมจำนวน 18 มุม ในรูปที่ 5.5<br />

จะแสดงรูปทรงของปิรามิดในขณะที่เคลื่อนไหวไปทั่วๆ จอ<br />

รูปที่ 5.5 ตัวอย่างของการใช้ไลบรารี OpenGL ES ในการสร้างรูปทรงปิรามิด<br />

พร้อมทั้งหมุนและเคลื่อนที่ไปทั่วจอภาพ<br />

ในขั้นตอนต่อไป เราจะใช้ไลบราลี OpenGL ES ในการสร้างคลาสของ GLSurfaceView.<br />

Renderer เพื่อสร้างพื้นผิวบนรูปทรงปิรามิดตามชุดคำสั่งที่ 5.18 ซึ่งเราจะใช้เมธอดจำนวน 3 ตัว<br />

ในการทำงานนี้<br />

m onSurfaceCreated() – ใช้เริ่มต้นการทำงานของเฟรมเวิร์ค OpenGL<br />

m onSurfaceChanged() – ใช้กำหนดจุดเริ่มต้นของวิวในตอนที่เฟรมเวิร์คเริ่มทำงาน<br />

หรือเมื่อมีการเปลี่ยนแปลงขนาดของวิว<br />

m onDrawFrame() - ใช้ในการวาดรูปกราฟิกของรูปทรงต่างๆ<br />

ชุดคำสั่งที่ 5.18 src/com/cookbook/open_gl/PyramidRenderer.java<br />

package com.cookbook.open_gl;<br />

import javax.microedition.khronos.egl.EGLConfig;<br />

import javax.microedition.khronos.opengles.GL10;<br />

import android.opengl.GLSurfaceView;<br />

/**<br />

* Render a tumbling Pyramid


ไลบรารีขั้นสูงที่ใช้ในส่วนการติดต่อกับผู้ใช้งาน<br />

143<br />

*/<br />

class PyramidRenderer implements GLSurfaceView.Renderer {<br />

public PyramidRenderer(boolean useTranslucentBackground) {<br />

mTranslucentBackground = useTranslucentBackground;<br />

mPyramid = new Pyramid();<br />

}<br />

public void onDrawFrame(GL10 gl) {<br />

/* clear the screen */<br />

gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);<br />

/* draw a pyramid rotating */<br />

gl.glMatrixMode(GL10.GL_MODELVIEW);<br />

gl.glLoadIdentity();<br />

gl.glTranslatef(mCenter[0], mCenter[1], mCenter[2]);<br />

gl.glRotatef(mAngle, 0, 1, 0);<br />

gl.glRotatef(mAngle*0.25f, 1, 0, 0);<br />

gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);<br />

gl.glEnableClientState(GL10.GL_COLOR_ARRAY);<br />

mPyramid.draw(gl);<br />

mAngle += mAngleDelta;<br />

/* draw it bouncing off the walls */<br />

mCenter[0] += mVel[0];<br />

mCenter[1] += mVel[1];<br />

}<br />

if(Math.abs(mCenter[0])>4.0f) {<br />

mVel[0] = -mVel[0];<br />

mAngleDelta=(float) (5*(0.5-Math.random()));<br />

}<br />

if(Math.abs(mCenter[1])>6.0f) {<br />

mVel[1] = -mVel[1];<br />

mAngleDelta=(float) (5*(0.5-Math.random()));<br />

}<br />

public void onSurfaceChanged(GL10 gl, int width, int height) {<br />

gl.glViewport(0, 0, width, height);<br />

/* Set a new projection when the viewport is resized */<br />

float ratio = (float) width / height;<br />

gl.glMatrixMode(GL10.GL_PROJECTION);


144 บทที่ 5 อีเวนต์ต่างๆ ที่เกิดขึ้นในส่วนการติดต่อกับผู้ใช้งาน<br />

}<br />

gl.glLoadIdentity();<br />

gl.glFrustumf(-ratio, ratio, -1, 1, 1, 20);<br />

public void onSurfaceCreated(GL10 gl, EGLConfig config) {<br />

gl.glDisable(GL10.GL_DITHER);<br />

/* one-time OpenGL initialization */<br />

gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,<br />

GL10.GL_FASTEST);<br />

}<br />

if (mTranslucentBackground) {<br />

gl.glClearColor(0,0,0,0);<br />

} else {<br />

gl.glClearColor(1,1,1,1);<br />

}<br />

gl.glEnable(GL10.GL_CULL_FACE);<br />

gl.glShadeModel(GL10.GL_SMOOTH);<br />

gl.glEnable(GL10.GL_DEPTH_TEST);<br />

}<br />

private boolean mTranslucentBackground;<br />

private Pyramid mPyramid;<br />

private float mAngle, mAngleDelta=0;<br />

private float mCenter[]={0,0,-10};<br />

private float mVel[]={0.025f, 0.03535227f, 0f};<br />

การเคลื่อนไหวของรูปทรงไปทั่วจอภาพนั้นเกิดจากการทำงานของเมธอด onDrawFrame()<br />

จอภาพจะถูกลบ เพื่อสร้างภาพเฟรมถัดไป ซึ่งเรากำหนดให้จุดศูนย์กลางของปิรามิดเก็บไว้ในตัวแปร<br />

ชื่อ mCenter[] โดยที่กึ่งกลางจอจะถูกกำหนดให้เป็นจุดเริ่มต้น และเมื่อเราสั่งให้จุดเริ่มต้นมีค่าเป็น<br />

(0,0,-10) รูปทรงก็จะแสดงที่ตำแหน่งขวาบนของจอ และในระหว่างที่แสดงภาพในแต่ละเฟรมนั้น<br />

รูปทรงจะหมุนด้วยการใช้ค่าของ mAngleDelta และ mVel[] โดยตัวแปร mVel[] จะเก็บค่าของ<br />

แกน x และ y ของตำแหน่งที่รูปทรงจะเคลื่อนที่ไป และเมื่อรูปทรงเคลื่อนที่ไปจนถึงของของจอภาพ<br />

ค่าของ mVel[] ก็จะเปลี่ยนไปในทิศทางอื่นเพื่อให้สามารถแสดงรูปทรงในเฟรมถัดไปได้<br />

ในชุดคำสั่งที่ 5.19 จะแสดงการทำงานของแอคทิวิตี้หลักที่จะแปลงข้อมูลภายในวิวให้เป็น<br />

ออบเจ็กต์ของ OpenGL ES โดยการเคลื่อนไหวของรูปทรงนี้ เราสามารถสั่งให้หยุดหรือทำต่อด้วย<br />

คำสั่งเหล่านี้ได้


ไลบรารีขั้นสูงที่ใช้ในส่วนการติดต่อกับผู้ใช้งาน<br />

ชุดคำสั่งที่ 5.19 src/com/cookbook/open_gl/OpenGlExample.java<br />

145<br />

package com.cookbook.open_gl;<br />

import android.app.Activity;<br />

import android.opengl.GLSurfaceView;<br />

import android.os.Bundle;<br />

/* Wrapper activity demonstrating the use of GLSurfaceView, a view<br />

* that uses OpenGL drawing into a dedicated surface. */<br />

public class OpenGlExample extends Activity {<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

}<br />

// Set our Preview view as the Activity content<br />

mGLSurfaceView = new GLSurfaceView(this);<br />

mGLSurfaceView.setRenderer(new PyramidRenderer(true));<br />

setContentView(mGLSurfaceView);<br />

@Override<br />

protected void onResume() {<br />

super.onResume();<br />

mGLSurfaceView.onResume();<br />

}<br />

@Override<br />

protected void onPause() {<br />

super.onPause();<br />

mGLSurfaceView.onPause();<br />

}<br />

}<br />

private GLSurfaceView mGLSurfaceView;


146 บทที่ 4 ส่วนการติดต่อกับผู้ใช้งาน (User Interface)


147<br />

บทที่ 6<br />

เทคนิคการทำงานร่วมกับมัลติมีเดีย<br />

ระบบปฏิบัติการแอนดรอยด์สนับสนุนการทำงานร่วมกับสื่อมัลติมีเดียหลายประเภท ในบทนี้จะ<br />

เป็นการแนะนำเทคนิคต่างๆ ที่ใช้ในการจัดการรูปภาพ การบันทึก เล่นไฟล์เสียง รวมถึงการบันทึกและ<br />

เล่นไฟล์วิดีโอ ซึ่งในระบบปฏิบัติการแอนดรอยด์มีไลบรารีที่รองรับการถอดรหัสหรือเล่นไฟล์มัลติมีเดีย<br />

อยู่หลายมาตรฐาน แต่ในแง่ของไลบรารีที่ใช้ในการเข้ารหัสข้อมูลเพื่อบันทึกไฟล์มัลติมีเดียนั้นยังมีไม่<br />

มากนัก ในตารางที่ 6.1 แสดงรายการของสื่อมัลติมีเดียที่รองรับการทำงานร่วมกับระบบปฏิบัติการ<br />

แอนดรอยด์เวอร์ชั่น 2.2 โดยในเวอร์ชั่นนี้ยังไม่รองรับการทำงานร่วมกับข้อมูลเสียงแบบบีบอัด ซึ่งจะมี<br />

การพัฒนาส่วนนี้เพิ่มเติมในเวอร์ชั่นถัดไป<br />

ตารางที่ 6.1 แสดงสื่อมัลติมีเดียที่รองรับการเขียนและอ่านบนระบบปฏิบัติการแอนดรอยด์<br />

เวอร์ชั่น 2.2<br />

ชนิดของสื่อ การบีบอัดข้อมูล การทางานบน<br />

แอนดรอยด์<br />

รูปภาพ ไม่มีการบีบอัด ดู BMP<br />

ชนิดของไฟล์<br />

การบีบอัดข้อมูลแบบไม่สูญเสีย ดู GIF , PNG<br />

การบีบอัดข้อมูลแบบสูญเสียบางส่วน เก็บข้อมูล / ดู JPEG<br />

เสียง ไม่มีการบีบอัด บันทึก / เล่น PCM<br />

(เพลง)<br />

ไม่มีการบีบอัด เล่น WAVE<br />

การบีบอัดข้อมูลแบบไม่สูญเสีย ไม่รองรับ เช่นไฟล์ FLAC<br />

การบีบอัดข้อมูลแบบสูญเสียบางส่วน เล่น MP3, MP4, AAC, HE-AACv1,<br />

HE-AACv2, Ogg Vorbis<br />

Midi เล่น MID, XMF, RTTTL, RTX, OTA,<br />

IMY<br />

เสียง การบีบอัดข้อมูลแบบสูญเสียบางส่วน บันทึก / เล่น AMR-NB<br />

(เสียงพูด)<br />

การบีบอัดข้อมูลแบบสูญเสียบางส่วน เล่น AMR-WB<br />

วิดีโอ การบีบอัดข้อมูลแบบแทบจะไม่สูญเสีย เล่น H.264<br />

การบีบอัดข้อมูลแบบสูญเสียบางส่วน บันทึก / เล่น H.263 , MPEG-4 SP


148 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

ถ้าเราจะพัฒนาแอพที่เกี่ยวข้องกับการบันทึกข้อมูลสื่อมัลติมีเดีย เราจะต้องกำหนดสิทธิ์การเข้า<br />

ถึงการทำงานของระบบปฏิบัติการในไฟล์ Android Manifest ตามนี้<br />

<br />

<br />

รูปภาพ<br />

การใช้รูปภาพในแอพ เราจะนำรูปภาพใส่ไว้ในไดเร็กทอรี res/drawable/ ซึ่งมันจะถูกรวมเข้า<br />

กับแอพขณะที่ทำการคอมไพล์ ในการเขียนชุดคำสั่งนั้น เราสามารถเรียกใช้รูปภาพเหล่านี้ได้ด้วยการ<br />

อ้างอิงรีซอร์ส เช่น R.drawable.my_picture ส่วนรูปภาพที่จัดเก็บอยู่ในระบบไฟล์ภายในหน่วย<br />

ความจำของอุปกรณ์แอนดรอยด์นั้น เราจะใช้งานด้วยคลาสจาวา เช่น InputStream แต่อย่างไร<br />

ก็ตามวิธีที่อยากแนะนำในการอ่านข้อมูลภาพเข้าสู่หน่วยความจำเพื่อปรับแต่งหรือแก้ไขนั้น เราจะใช้<br />

คลาสที่มีอยู่ในระบบปฏิบัติการแอนดรอยด์อยู่แล้ว คือคลาส BitmapFactory<br />

คลาส BitmapFactory จะใช้ในการสร้างออบเจ็กต์ของบิตแม็พด้วยข้อมูลจากไฟล์หรืออาร์เรย์<br />

ต่างๆ ดังคำสั่งนี้<br />

Bitmap myBitmap1 = BitmapFactory.decodeResource(getResources(),<br />

R.drawable.my_picture);<br />

Bitmap myBitmap2 = BitmapFactory.decodeFile(filePath);<br />

หลังจากที่ข้อมูลรูปภาพถูกอ่านเข้าสู่หน่วยความจำแล้ว ข้อมูลเหล่านี้ก็จะอยู่ในสถานะที่สามารถ<br />

ใช้เมธอดที่เกี่ยวข้องเช่น getPixel() และ setPixel() มาจัดการรูปภาพได้ แต่ในความเป็นจริงนั้น<br />

รูปภาพส่วนใหญ่มักจะมีขนาดใหญ่เกินกว่าที่จะนำข้อมูลทั้งหมดไปไว้ในหน่วยความจำได้ ซึ่งจะมีปัญหา<br />

กับอุปกรณ์แอนดรอยด์ที่มีหน่วยความจำไม่มาก ดังนั้นเราจะใช้วิธีอ่านบางส่วนของภาพเข้าสู่หน่วย<br />

ความจำแทน ดังนี้<br />

Bitmap bm = Bitmap.createScaledBitmap(myBitmap2, 480, 320, false);<br />

จากคำสั่งข้างต้นจะช่วยหลีกเลี่ยงการเกิดข้อผิดพลาดในกรณีที่หน่วยความจำไม่พอได้<br />

กรรมวิธี: การจัดการไฟล์รูปภาพ<br />

ในส่วนนี้จะเป็นการแสดงตัวอย่างของการตัดรูปภาพออกเป็น 4 ชิ้น และสลับก่อนที่จะนำไป<br />

แสดงผลบนจอภาพ ซึ่งจะแสดงวิธีการสร้างรายการของรูปภาพที่เลือกได้<br />

เมื่ออุปกรณ์แอนดรอยด์มีการถ่ายรูป รูปที่ถ่ายจะเก็บลงในไดเร็กทอรี DCIM/Camera/ ซึ่งเรา<br />

จะหยิบรูปภาพในไดเร็กทอรีนี้มาใช้ประกอบการทำงานในหัวข้อนี้ โดยจะส่งค่าของไดเร็กทอรีที่เก็บ<br />

รูปภาพไปยังแอคทิวิตี้ ListFiles ซึ่งจะแสดงไฟล์ทั้งหมดภายในไดเร็กทอรี และส่งค่าของไฟล์ที่ผู้ใช้<br />

งานเลือกไว้<br />

รูปภาพที่ถูกเลือกก็จะถูกอ่านเข้าสู่หน่วยความจำ ถ้ารูปภาพมีขนาดใหญ่มาก เราก็จะเลือกให้<br />

อ่านแค่บางส่วนเข้าสู่หน่วยความจำแทนด้วยการใช้คำสั่งใน onActivityResult ดังนี้<br />

BitmapFactory.Options options = new BitmapFactory.Options();<br />

options.inSampleSize = 4;<br />

Bitmap ImageToChange= BitmapFactory.decodeFile(tmp, options);<br />

ค่าของ inSampleSize นี้สร้างรูปภาพที่มีขนาดเล็กกว่ารูปภาพต้นฉบับ 4 เท่าเมื่อวัดจากขนาด<br />

ของจำนวนพิกเซลในแต่ละด้าน ซึ่งเราสามารถปรับเปลี่ยนค่านี้ได้ตามต้องการเพื่อให้สัมพันธ์กับขนาด<br />

ภาพต้นฉบับ


รูปภาพ<br />

149<br />

ส่วนอีกวิธีที่ช่วยประหยัดหน่วยความจำได้ก็คือการปรับขนาดของรูปภาพที่อยู่ในหน่วยความจำ<br />

ก่อน แล้วค่อยแก้ไขภาพดังกล่าว ขั้นตอนเหล่านี้เราจะใช้เมธอด createScaledBitmap() มาช่วยใน<br />

การทำงาน ดังชุดคำสั่งที่ 6.1<br />

ชุดคำสั่งที่ 6.1 src/com/cookbook/image_manip/ImageManipulation.java<br />

package com.cookbook.image_manip;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.graphics.Bitmap;<br />

import android.graphics.BitmapFactory;<br />

import android.os.Bundle;<br />

import android.os.Environment;<br />

import android.widget.ImageView;<br />

public class ImageManipulation extends Activity {<br />

static final String CAMERA_PIC_DIR = "/DCIM/Camera/";<br />

ImageView iv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

iv = (ImageView) findViewById(R.id.my_image);<br />

String ImageDir = Environment.getExternalStorageDirectory()<br />

.getAbsolutePath() + CAMERA_PIC_DIR;<br />

}<br />

Intent i = new Intent(this, ListFiles.class);<br />

i.putExtra("directory”, ImageDir);<br />

startActivityForResult(i,0);<br />

@Override<br />

protected void onActivityResult(int requestCode,<br />

int resultCode, Intent data) {<br />

super.onActivityResult(requestCode, resultCode, data);<br />

if(requestCode == 0 && resultCode==RESULT_OK) {<br />

String tmp = data.getExtras().getString("clickedFile");<br />

Bitmap ImageToChange= BitmapFactory.decodeFile(tmp);<br />

process_image(ImageToChange);<br />

}<br />

}<br />

void process_image(Bitmap image) {<br />

Bitmap bm = Bitmap.createScaledBitmap(image, 480, 320, false);


}<br />

150 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

}<br />

int width = bm.getWidth();<br />

int height = bm.getHeight();<br />

int x= width>>1;<br />

int y= height>>1;<br />

int[] pixels1 = new int[(width*height)];<br />

int[] pixels2 = new int[(width*height)];<br />

int[] pixels3 = new int[(width*height)];<br />

int[] pixels4 = new int[(width*height)];<br />

bm.getPixels(pixels1, 0, width, 0, 0, width>>1, height>>1);<br />

bm.getPixels(pixels2, 0, width, x, 0, width>>1, height>>1);<br />

bm.getPixels(pixels3, 0, width, 0, y, width>>1, height>>1);<br />

bm.getPixels(pixels4, 0, width, x, y, width>>1, height>>1);<br />

if(bm.isMutable()) {<br />

bm.setPixels(pixels2, 0, width, 0, 0, width>>1, height>>1);<br />

bm.setPixels(pixels4, 0, width, x, 0, width>>1, height>>1);<br />

bm.setPixels(pixels1, 0, width, 0, y, width>>1, height>>1);<br />

bm.setPixels(pixels3, 0, width, x, y, width>>1, height>>1);<br />

}<br />

iv.setImageBitmap(bm);<br />

ชุดคำสั่งนี้จะทำงานร่วมกับเลย์เอาต์ตามที่แสดงในชุดคำสั่งที่ 6.2<br />

ชุดคำสั่งที่ 6.2 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

ในชุดคำสั่งที่ 6.3 แอคทิวิตี้ตัวที่ 2 จะแสดงรายการของไฟล์ในไดเร็กทอรีที่ระบุไว้ โดยจะสร้าง<br />

ออบเจ็กต์ File จากค่าของไดเร็กทอรี ถ้ารายการไฟล์ในไดเร็กทอรีมีการเรียงลำดับไว้ รายการของ<br />

ไฟล์ที่เก็บอยู่ในออบเจ็กต์ก็จะมีลำดับเช่นนั้นด้วย


รายการของไฟล์จะสร้างขึ้นไว้ภายในออบเจ็กต์ และนำมาแสดงบนจอภาพโดยใช้ไฟล์เลย์เอาต์<br />

ชื่อ R.layout.file_row ตามที่แสดงในชุดคำสั่งที่ 6.4<br />

ชุดคำสั่งที่ 6.3 src/com/cookbook/image_manip/ListFiles.java<br />

รูปภาพ<br />

151<br />

package com.cookbook.image_manip;<br />

import java.io.File;<br />

import java.util.ArrayList;<br />

import java.util.Arrays;<br />

import java.util.Comparator;<br />

import java.util.List;<br />

import android.app.ListActivity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.ArrayAdapter;<br />

import android.widget.ListView;<br />

public class ListFiles extends ListActivity {<br />

private List directoryEntries = new ArrayList();<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

Intent i = getIntent();<br />

File directory = new File(i.getStringExtra("directory"));<br />

if (directory.isDirectory()){<br />

File[] files = directory.listFiles();<br />

//sort in descending date order<br />

Arrays.sort(files, new Comparator(){<br />

public int compare(File f1, File f2) {<br />

return -Long.valueOf(f1.lastModified())<br />

.compareTo(f2.lastModified());<br />

}<br />

});<br />

//fill list with files<br />

this.directoryEntries.clear();<br />

for (File file : files){<br />

this.directoryEntries.add(file.getPath());<br />

}


152 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

ArrayAdapter directoryList<br />

= new ArrayAdapter(this,<br />

R.layout.file_row, this.directoryEntries);<br />

}<br />

}<br />

//alphabetize entries<br />

//directoryList.sort(null);<br />

this.setListAdapter(directoryList);<br />

}<br />

@Override<br />

protected void onListItemClick(ListView l, View v, int pos, long id) {<br />

File clickedFile = new File(this.directoryEntries.get(pos));<br />

Intent i = getIntent();<br />

i.putExtra("clickedFile", clickedFile.toString());<br />

setResult(RESULT_OK, i);<br />

finish();<br />

}<br />

ไฟล์เลย์เอาต์ที่เกี่ยวข้องกับแอคทิวิตี้ ListFiles แสดงอยู่ในชุดคำสั่งที่ 6.4 โดยไฟล์<br />

AndroidManifest จะต้องประกาศการใช้แอคทิวิตี้ทั้ง 2 ตัวนี้ และผลลัพธ์ของกระบวนการนี้แสดงไว้<br />

ในรูปที่ 6.1<br />

ชุดคำสั่งที่ 6.4 res/layout/file_row.xml<br />

<br />

<br />

ชุดคำสั่งที่ 6.5 AndroidManifest.xml<br />

<br />

<br />

<br />


รูปภาพ<br />

153<br />

android:label="@string/app_name"><br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

รูปที่ 6.1 ตัวอย่างของรูปภาพที่ถูกแบ่งเป็น 4 ส่วน<br />

และสลับที่กัน


154 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

เสียง<br />

การบันทึกและเล่นข้อมูลเสียงจะมีเฟรมเวิร์คอยู่ 2 ชนิดที่เรียกใช้งานได้ ซึ่งการเลือกใช้นั้นจะ<br />

ขึ้นอยู่กับประเภทการทำงานของแอพ ซึ่งได้แก่<br />

m MediaPlayer/MediaRecorder – เมธอดมาตรฐานที่ใช้จัดการข้อมูลเสียง ซึ่งจะอยู่ในรูป<br />

ของไฟล์หรือข้อมูลแบบสตรีมก็ได้เวลาทำงานเฟรมเวิร์คนี้จะสร้างเธรดเป็นของตัวเอง<br />

m AudioTrack/AudioRecorder – เฟรมเวิร์คชนิดนี้สามารถเข้าถึงข้อมูลเสียงแบบ Raw<br />

ได้โดยตรง โดยจะทำงานอยู่บนหน่วยความจำ และจะส่งข้อมูลไปยังส่วนบัฟเฟอร์เมื่ออยู่<br />

ในสถานะพร้อมใช้งานโดยไม่จำเป็นต้องจัดเก็บอยู่ในรูปของไฟล์ก่อนใช้งานได้<br />

เวลาทำงานเฟรมเวิร์คนี้จะไม่สร้างเธรดของตัวเอง<br />

การใช้งานเมธอดเหล่านี้จะกล่าวถึงในหัวข้อถัดไป<br />

กรรมวิธี: การสั่งให้เล่นไฟล์เสียงที่เลือกไว้<br />

เราจะใช้คลาส MediaRecorder และ MediaPlayer ในการบันทึกและเล่นข้อมูลเสียง<br />

และข้อมูลวิดีโอ ซึ่งในหัวข้อนี้เราจะทดลองการใช้งานร่วมกับข้อมูลเสียง โดยมีขั้นตอนการทำงานดังนี้<br />

1. สร้างอินสแตนซ์จากคลาส MediaPlayer<br />

MediaPlayer m_mediaPlayer = new MediaPlayer();<br />

2. กำหนดแหล่งของข้อมูลที่จะใช้งาน ซึ่งสามารถสร้างจากข้อมูลเสียงแบบ Raw ได้<br />

m_mediaPlayer = MediaPlayer.create(this, R.raw.my_music);<br />

นอกเหนือจากนั้น เราสามารถใช้ข้อมูลประเภทสตรีมได้เช่นกัน แต่จะต้องใช้คำสั่ง<br />

เพิ่มเติมเพื่อเตรียมการทำงานดังนี้<br />

m_mediaPlayer.setDataSource(path);<br />

m_mediaPlayer.prepare();<br />

ในการทำงานของชุดคำสั่งนั้น เราจะต้องใช้คำสั่ง try-catch มาครอบไว้เพื่อป้องกันข้อ<br />

ผิดพลาดที่อาจเกิดขึ้นในกรณีที่ระบบหาแหล่งข้อมูลไม่พบ<br />

3. เริ่มต้นเล่นข้อมูลเสียง<br />

m_mediaPlayer.start();<br />

4. เมื่อเล่นข้อมูลเสียงเสร็จแล้วก็จะหยุดการทำงานของ MediaPlayer และยกเลิก<br />

อินสแตนซ์เพื่อคืนหน่วยความจำกลับสู่ระบบ<br />

m_mediaPlayer.stop();<br />

m_mediaPlayer.release();<br />

ในหัวข้อนี้เรายังคงใช้คำสั่ง ListFiles จากในชุดคำสั่งที่ 6.3 ละ 6.4 มาใช้ในการสร้าง<br />

รายการของไฟล์ที่จะเอามาเล่น ในการทำงานนี้เราจะเก็บไฟล์เสียงที่จะใช้ไว้ในไดเร็กทอรี /sdcard/<br />

music/ ซึ่งไดเร็กทอรีนี้สามารถเปลี่ยนในภายหลังได้ตามความต้องการ<br />

เมื่อคำสั่ง ListFiles ได้ส่งค่าของไฟล์ที่ต้องการจะเล่นกลับมายังระบบแล้ว เราก็จะเริ่มต้น<br />

การทำงานของ MediaPlayer และสั่งให้เมธอด startMP() เริ่มทำงาน โดยหลังจากเริ่มทำงานแล้ว<br />

จะปรากฏปุ่มบนจอภาพ ซึ่งแสดงข้อความบนปุ่มว่า Pause


155<br />

เราจะใช้เมธอด pauseMP() เพื่อหยุดการทำงานของ MediaPlayer และเปลี่ยนข้อความบน<br />

ปุ่มเป็น Play ซึ่งผู้ใช้สามารถกดปุ่มดังกล่าวเพื่อเล่นเพลงหรือหยุดเพลงได้ตามต้องการ<br />

โดยปกติแล้ว MediaPlayer จะสร้างเธรดของมันเองขึ้นมาเพื่อรองรับการทำงานอยู่เบื้องหลัง<br />

และเธรดนี้จะไม่หยุดทำงานไปจนกว่าแอคทิวิตี้จะหยุดทำงาน เหตุผลที่มีการทำงานลักษณะนี้ก็เพราะ<br />

ว่าการทำงานดังกล่าวจะเหมาะกับการเล่นข้อมูลเสียง และเพื่อให้ผู้พัฒนาสามารถควบคุมการทำงาน<br />

ต่างๆ เพิ่มติมในระหว่างที่กำลังเล่นไฟล์เสียงได้นั่นเอง ดังนั้นเราจะมาสั่งให้ MadiaPlayer เล่นหรือ<br />

หยุดเล่นไฟล์เสียงโดยใช้คำสั่ง onPause() และ onResume() ในแอคทิวิตี้หลักดังแสดงในชุดคำสั่งที่<br />

6.6 กัน<br />

ชุดคำสั่งที่ 6.6 src/com/cookbook/audio_ex/AudioExamples.java<br />

เสียง<br />

package com.cookbook.audio_ex;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.media.MediaPlayer;<br />

import android.os.Bundle;<br />

import android.os.Environment;<br />

import android.view.View;<br />

import android.widget.Button;<br />

public class AudioExamples extends Activity {<br />

static final String MUSIC_DIR = "/music/";<br />

Button playPauseButton;<br />

private MediaPlayer m_mediaPlayer;<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

playPauseButton = (Button) findViewById(R.id.play_pause);<br />

m_mediaPlayer= new MediaPlayer();<br />

String MusicDir = Environment.getExternalStorageDirectory()<br />

.getAbsolutePath() + MUSIC_DIR;<br />

//Show a list of Music files to choose<br />

Intent i = new Intent(this, ListFiles.class);<br />

i.putExtra("directory", MusicDir);<br />

startActivityForResult(i,0);<br />

playPauseButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

if(m_mediaPlayer.isPlaying()) {


156 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

}<br />

}<br />

});<br />

//stop and give option to start again<br />

pauseMP();<br />

} else {<br />

startMP();<br />

}<br />

@Override<br />

protected void onActivityResult(int requestCode,<br />

int resultCode, Intent data) {<br />

super.onActivityResult(requestCode, resultCode, data);<br />

if(requestCode == 0 && resultCode==RESULT_OK) {<br />

String tmp = data.getExtras().getString("clickedFile");<br />

}<br />

}<br />

try {<br />

m_mediaPlayer.setDataSource(tmp);<br />

m_mediaPlayer.prepare();<br />

} catch (Exception e) {<br />

e.printStackTrace();<br />

}<br />

startMP();<br />

void pauseMP() {<br />

playPauseButton.setText("Play");<br />

m_mediaPlayer.pause();<br />

}<br />

void startMP() {<br />

m_mediaPlayer.start();<br />

playPauseButton.setText("Pause");<br />

}<br />

boolean needToResume = false;<br />

@Override<br />

protected void onPause() {<br />

if(m_mediaPlayer != null && m_mediaPlayer.isPlaying()) {<br />

needToResume = true;<br />

pauseMP();<br />

}<br />

super.onPause();<br />

}


}<br />

@Override<br />

protected void onResume() {<br />

super.onResume();<br />

if(needToResume && m_mediaPlayer != null) {<br />

startMP();<br />

}<br />

}<br />

เสียง<br />

157<br />

เลย์เอาต์ XML ตัวหลักพร้อมกับปุ่ม Play/Pause นั้นดูได้จากชุดคำสั่งที่ 6.7<br />

ชุดคำสั่งที่ 6.7 แสดงไฟล์เลย์เอาต์ที่แสดงปุ่มเล่นและหยุดเล่นไฟล์เสียง<br />

<br />

<br />

<br />

<br />

กรรมวิธี: การบันทึกไฟล์เสียง<br />

การบันทึกเสียงด้วยการใช้คำสั่ง MediaRecorder นั้นจะคล้ายๆ กับการใช้คำสั่ง MediaPlayer<br />

ในหัวข้อก่อนหน้านี้ เพียงแต่ว่าเราจะต้องกำหนดการทำงานบางอย่างเพิ่มเติมก่อนที่จะใช้งาน ดังนี้<br />

m MediaRecorder.AudioSource :<br />

m MIC – ไมโครโฟนในตัวเครื่อง<br />

m VOICE_UPLINK – ส่งข้อมูลเสียงในขณะที่กำลังเรียกสาย<br />

m VOICE_DOWNLINK – รับข้อมูลเสียงในขณะที่กำลังเรียกสาย<br />

m VOICE_CALL – รับและส่งข้อมูลเสียงในขณะที่กำลังเรียกสาย<br />

m CAMCORDER – ไมโครโฟนที่ทำงานร่วมกับกล้องถ่ายรูป (ถ้ามี)<br />

m VOICE_RECOGNITION – ปรับการทำงานของไมโครโฟนเพื่อรองรับการวิเคราะห์เสียง<br />

(ถ้ามี)<br />

m MediaRecorder.OutputFormat<br />

m THREE_GPP – ไฟล์ประเภท 3GPP<br />

m MPEG_4 – ไฟล์ประเภท MPEG4<br />

m AMR_NB – ไฟล์ประเภทช่วงความถี่เสียงแคบ


158 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

m MediaRecorder.AudioEncoder :<br />

m AMR_NB – การเข้ารหัสข้อมูลประเภทช่วงความถี่เสียงแคบ<br />

ขั้นตอนในการบันทึกเสียง มีดังนี้<br />

1. สร้างอินสแตนซ์ของ MediaRecorder<br />

MediaRecorder m_Recorder = new MediaRecorder();<br />

2. กำหนดแหล่งข้อมูลที่จะใช้งาน อย่างเช่น ไมโครโฟน<br />

m_Recorder.setAudioSource(MediaRecorder.AudioSource.MIC);<br />

3. กำหนดประเภทของไฟล์เอาต์พุต และรูปแบบการเข้ารหัส<br />

m_Recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);<br />

m_Recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);<br />

4. กำหนดไดเร็กทอรีที่จะใช้ในการจัดเก็บข้อมูลที่บันทึก<br />

m_Recorder.setOutputFile(path);<br />

5. เริ่มการบันทึก<br />

m_Recorder.prepare();<br />

m_Recorder.start();<br />

เมื่อจบขั้นตอนการบันทึกเสียงแล้วก็สามารถใช้ชุดค ำสั่งก่อนหน้านี้มาเล่นไฟล์เสียงที่บันทึกไว้นี้ได้<br />

กรรมวิธี: การจัดการข้อมูลเสียงแบบ Raw<br />

เฟรมเวิร์ค MediaRecorder/MediaPlayer มักจะมีการใช้ในแอพที่ทำงานกับเสียงเป็นส่วน<br />

ใหญ่ แต่การที่เราจะทำงานกับข้อมูลเสียงแบบ Raw ซึ่งเป็นข้อมูลที่ส่งมาจากไมโครโฟนโดยตรงจะ<br />

ไม่มีการเก็บข้อมูลลงไฟล์นั้น เราจะใช้คำสั่ง AudioRecord และ AudioTrack แทน โดยในขั้นแรก<br />

เราต้องกำหนดสิทธิ์การบันทึกเสียงลงในไฟล์ Manifest ก่อนดังนี้<br />

<br />

และขั้นตอนการทำงานจะมีดังนี้<br />

1. สร้างอินสแตนซ์ของคลาส AudioRecord โดยกำหนดรายละเอียดดังนี้<br />

m แหล่งข้อมูลเสียง – ใช้คำสั่ง MediaRecorder.AudioSource ในการกำหนดแหล่ง<br />

ของข้อมูลอินพุตดังนี้<br />

MediaRecorder.AudioSource.MIC.<br />

m การผสมความถี่เสียง – ถ้าต้องการคุณภาพเสียงใกล้เคียงกับ CD ก็ให้ใช้ค่าเป็น<br />

44100 Hz หรือกำหนดเป็น 22050 Hz หรือ 11025 Hz ในระดับคุณภาพที่รองลงมา<br />

m ช่องสัญญาณเสียง – ใช้คำสั่ง AudioFormat.CHANNEL_IN_STEREO เพื่อบันทึก<br />

เสียงแบบสเตอริโอ หรือใช้คำสั่ง AudioFormat.CHANNEL_IN_MONO เพื่อบันทึก<br />

เสียงแบบโมโน<br />

m การเข้ารหัสข้อมูลเสียง – ใช้คำสั่ง AudioFormat.ENCODING_PCM_8BIT เพื่อเข้า<br />

รหัสข้อมูลแบบ 8 บิต หรือใช้คำสั่ง AudioFormat.ENCODING_PCM_16BIT<br />

เพื่อเข้ารหัสข้อมูลแบบ 16 บิต


2. เริ่มต้นบันทึก โดยเริ่มการทำงานของ AudioRecord<br />

3. อ่านข้อมูลเสียงเข้าสู่หน่วยความจำไว้ในตัวแปร audioData[] ด้วยคำสั่งนี้<br />

read(short[] audioData, int offsetInShorts, int sizeInShorts)<br />

read(byte[] audioData, int offsetInBytes, int sizeInBytes)<br />

read(ByteBuffer audioData, int sizeInBytes)<br />

4. หยุดการบันทึก<br />

ในตัวอย่างนี้ เราจะบันทึกข้อมูลเสียงจากไมโครโฟนที่ติดตั้งอยู่ในอุปกรณ์แอนดรอยด์ลงไปยัง<br />

บัฟเฟอร์ myRecordedAudio ซึ่งมีขนาดของข้อมูลเป็น short[] (เพื่อรองรับขนาดของข้อมูลแบบ<br />

16 บิต) เรากำหนดให้ผสมคลื่นความถี่เสียงด้วยอัตรา 11,025 รอบต่อวินาที (Hz) และขนาดของ<br />

บัฟเฟอร์รองรับที่ 10,000 รอบต่อวินาที<br />

159<br />

m ขนาดของบัฟเฟอร์ – กำหนดค่าของบัฟเฟอร์ที่จะใช้ในการทำงาน ซึ่งจะใช้กับข้อมูล<br />

ประเภทไฟล์หรือข้อมูลสตรีม หน่วยของบัฟเฟอร์จะมีขนาดเป็นไบต์ โดยกำหนดให้มี<br />

ค่าสูงกว่าค่าของ getMinBufferSize()<br />

short[] myRecordedAudio = new short[10000];<br />

AudioRecord audioRecord = new AudioRecord(<br />

MediaRecorder.AudioSource.MIC, 11025,<br />

AudioFormat.CHANNEL_IN_MONO,<br />

AudioFormat.ENCODING_PCM_16BIT, 10000);<br />

audioRecord.startRecording();<br />

audioRecord.read(myRecordedAudio, 0, 10000);<br />

audioRecord.stop();<br />

เสียง<br />

ขั้นตอนในการเล่นข้อมูลเสียงที่บันทึกไว้มีดังนี้<br />

1. สร้างอินสแตนซ์ AudioTrack และกำหนดค่าดังนี้<br />

m ชนิดของสตรีม –ใช้คำสั่ง AudioManager.STREAM_MUSIC เพื่อจับข้อมูลเสียงจาก<br />

ไมโครโฟน หรือใช้เล่นเสียงไปยังลำโพง ข้อมูลสตรีมชนิดอื่นๆ ที่สามารถเลือกใช้ได้<br />

คือ STREAM_VOICE_CALL, STREAM_SYSTEM, STREAM_RING และ STREAM_<br />

ALARM<br />

m การผสมความถี่เสียง – ถ้าต้องการคุณภาพเสียงใกล้เคียงกับ CD ก็จะใช้ค่าเป็น<br />

44100 Hz หรือกำหนดเป็น 22050 Hz หรือ 11025 Hz ในระดับคุณภาพที่รองลงมา<br />

m ช่องสัญญาณเสียง – ใช้คำสั่ง AudioFormat.CHANNEL_OUT_STEREO เพื่อบันทึก<br />

เสียงแบบสเตอริโอ หรือใช้คำสั่ง AudioFormat.CHANNEL_OUT_MONO เพื่อบันทึก<br />

เสียงแบบโมโน และ CHANNEL_OUT_5POINT เพื่อบันทึกเสียงแบบเซอร์ราวด์<br />

m การเข้ารหัสข้อมูลเสียง – ใช้คำสั่ง AudioFormat.ENCODING_PCM_8BIT เพื่อเข้า<br />

รหัสข้อมูลแบบ 8 บิต หรือใช้คำสั่ง AudioFormat.ENCODING_PCM_16BIT<br />

เพื่อเข้ารหัสข้อมูลแบบ 16 บิต<br />

m ขนาดของบัฟเฟอร์ – กำหนดค่าของบัฟเฟอร์ที่จะใช้ในการทำงาน ซึ่งจะใช้งานกับ<br />

ข้อมูลประเภทไฟล์หรือข้อมูลสตรีม หน่วยของบัฟเฟอร์จะมีขนาดเป็นไบต์<br />

โดยกำหนดให้มีค่าสูงกว่าค่าของ getMinBufferSize()<br />

m ชนิดของบัฟเฟอร์ – ใช้คำสั่ง AudioTrack.MODE_STATIC สำหรับข้อมูลเสียงที่ไม่<br />

ยาวนักที่สามารถใส่ลงในหน่วยความจำได้ทั้งหมด ส่วนข้อมูลเสียงที่มีขนาดใหญ่เราจะ<br />

ต้องอ่านข้อมูลเป็นส่วนๆ เข้าสู่หน่วยความจำด้วยคำสั่ง AudioTrack.MODE_STREAM


160 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

2. เริ่มเล่นข้อมูลเสียงจากอินสแตนซ์ AudioTrack<br />

3. ส่งค่าของตัวแปร audioData[] ไปยังอุปกรณ์โดยใช้คำสั่งตามนี้<br />

write(short[] audioData, int offsetInShorts, int sizeInShorts)<br />

write(byte[] audioData, int offsetInBytes, int sizeInBytes)<br />

4. หยุดการเล่นข้อมูลเสียง<br />

ด้านล่างนี้เป็นตัวอย่างของการเล่นข้อมูลเสียงจากข้อมูลที่ได้บันทึกไว้ก่อนหน้า<br />

AudioTrack audioTrack = new AudioTrack(<br />

AudioManager.STREAM_MUSIC, 11025,<br />

AudioFormat.CHANNEL_OUT_MONO,<br />

AudioFormat.ENCODING_PCM_16BIT, 4096,<br />

AudioTrack.MODE_STREAM);<br />

audioTrack.play();<br />

audioTrack.write(myRecordedAudio, 0, 10000);<br />

audioTrack.stop();<br />

ตัวอย่างนี้ใช้เมธอดจำนวน 2 ตัวในการบันทึกข้อมูลเสียงลงไปในหน่วยความจำและเล่นข้อมูล<br />

ดังกล่าว ไฟล์เลย์เอาต์ที่แสดงการสร้างปุ่มจำนวน 2 ปุ่มบนจอภาพ ซึ่งปุ่มแรกจะใช้ในการบักทึกเสียง<br />

และปุ่มที่ 2 จะใช้ในการเล่นเสียงที่บันทึกไว้ แสดงไว้แล้วในชุดคำสั่งที่ 6.8<br />

ชุดคำสั่งที่ 6.8 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

แอคทิวิตี้หลักที่แสดงในชุดคำสั่งที่ 6.9 นั้น ในขั้นแรกจะสร้างอีเวนต์ onClickListener<br />

เพื่อตรวจจับการทำงานของปุ่มบันทึกเสียงหรือเล่นเสียง ซึ่งเมื่อมีอีเวนต์เกิดขึ้น เมธอด onClick()<br />

ก็จะเริ่มทำงาน โดยสร้างเธรดแบบเบื้องหลังขึ้นมาเพื่อรองรับการทำงานของ AudioTrack หรือ<br />

AudioRecord


ขั้นตอนการสร้างเธรดเพื่อทำงานนั้นประกอบด้วยการทำงานของเมธอดจำนวน 2 ตัว คือ<br />

record_thread() ซึ่งจะทำงานบนเธรดหลักเพื่อควบคุมการทำงานบนจอภาพ และเมธอด run()<br />

ที่ทำงานบนเธรดย่อยเพื่อควบคุมการบันทึกและเล่นเสียง<br />

ข้อมูลบัฟเฟอร์จะเก็บอยู่ในหน่วยความจำ จากตัวอย่างนี้ ชุดคำสั่งจะรองรับการเก็บข้อมูลได้<br />

5 วินาที<br />

ชุดคำสั่งที่ 6.9 src/com/cookbook/audio_ex/AudioExamplesRaw.java<br />

เสียง<br />

161<br />

package com.cookbook.audio_ex;<br />

import android.app.Activity;<br />

import android.media.AudioFormat;<br />

import android.media.AudioManager;<br />

import android.media.AudioRecord;<br />

import android.media.AudioTrack;<br />

import android.media.MediaRecorder;<br />

import android.os.Bundle;<br />

import android.os.Handler;<br />

import android.util.Log;<br />

import android.view.View;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

public class AudioExamplesRaw extends Activity implements Runnable {<br />

private TextView statusText;<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

statusText = (TextView) findViewById(R.id.status);<br />

Button actionButton = (Button) findViewById(R.id.record);<br />

actionButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

record_thread();<br />

}<br />

});<br />

}<br />

Button replayButton = (Button) findViewById(R.id.play);<br />

replayButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

Thread thread = new Thread(AudioExamplesRaw.this);<br />

thread.start();<br />

}<br />

});


162 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

String text_string;<br />

final Handler mHandler = new Handler();<br />

// Create runnable for posting<br />

final Runnable mUpdateResults = new Runnable() {<br />

public void run() {<br />

updateResultsInUi(text_string);<br />

}<br />

};<br />

private void updateResultsInUi(String update_txt) {<br />

statusText.setText(update_txt);<br />

}<br />

private void record_thread() {<br />

Thread thread = new Thread(new Runnable() {<br />

public void run() {<br />

text_string = "Starting";<br />

mHandler.post(mUpdateResults);<br />

record();<br />

}<br />

text_string = "Done";<br />

mHandler.post(mUpdateResults);<br />

}<br />

});<br />

thread.start();<br />

private int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;<br />

int frequency = 11025; //Hz<br />

int bufferSize = 50*AudioTrack.getMinBufferSize(frequency,<br />

AudioFormat.CHANNEL_OUT_MONO, audioEncoding);<br />

// Create new AudioRecord object to record the audio.<br />

public AudioRecord audioRecord = new AudioRecord(<br />

MediaRecorder.AudioSource.MIC,<br />

frequency, AudioFormat.CHANNEL_IN_MONO,<br />

audioEncoding, bufferSize);<br />

// Create new AudioTrack object w/same parameters as AudioRecord obj<br />

public AudioTrack audioTrack = new AudioTrack(<br />

AudioManager.STREAM_MUSIC, frequency,<br />

AudioFormat.CHANNEL_OUT_MONO,<br />

audioEncoding, 4096,<br />

AudioTrack.MODE_STREAM);<br />

short[] buffer = new short[bufferSize];<br />

public void record() {<br />

try {<br />

audioRecord.startRecording();


}<br />

audioRecord.read(buffer, 0, bufferSize);<br />

audioRecord.stop();<br />

} catch (Throwable t) {<br />

Log.e("AudioExamplesRaw","Recording Failed");<br />

}<br />

เสียง<br />

163<br />

public void run() { //play audio using runnable Activity<br />

audioTrack.play();<br />

//this alone works: audioTrack.write(buffer, 0, bufferSize);<br />

//but for illustration showing another way to play using a loop:<br />

int i=0;<br />

while(i


164 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

ในหัวข้อนี้เราจะใช้ไฟล์เลย์เอาต์ที่แสดงในชุดคำสั่งที่ 6.7 โดยแอคทิวิตี้หลักจะแสดงไว้ในชุด<br />

คำสั่งที่ 6.10 เวลาที่กดปุ่ม SoundPool ก็จะเล่นเสียงกลองจำนวน 8 ครั้ง (เล่นในขั้นตอนเริ่มทำงาน<br />

1 ครั้ง และสั่งให้ทำซ้ำอีก 7 ครั้ง) เราสามารถเล่นข้อมูลสตรีมนี้ได้พร้อมกัน 10 สตรีม ซึ่งหมายความ<br />

ว่าถ้าเรากดปุ่มอย่างรวดเร็วจำนวน 10 ครั้ง ระบบก็จะสร้างเธรดเพื่อเล่นเสียงกลองพร้อมกันทั้งหมด<br />

ชุดคำสั่งที่ 6.10 src/com/cookbook/audio_ex/AudioExamplesSP.java<br />

package com.cookbook.audio_ex;<br />

import android.app.Activity;<br />

import android.media.AudioManager;<br />

import android.media.SoundPool;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.widget.Button;<br />

public class AudioExamplesSP extends Activity {<br />

static float rate = 0.5f;<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Button playDrumButton = (Button) findViewById(R.id.play_pause);<br />

final SoundPool mySP = new<br />

SoundPool(10, AudioManager.STREAM_MUSIC, 0);<br />

final int soundId = mySP.load(this, R.raw.drum_beat, 1);<br />

}<br />

}<br />

playDrumButton.setOnClickListener(new View.OnClickListener() {<br />

public void onClick(View view) {<br />

rate = 1/rate;<br />

mySP.play(soundId, 1f, 1f, 1, 7, rate);<br />

}<br />

});


วิดีโอ<br />

165<br />

กรรมวิธี: การเพิ่มรายการเสียงและการอัพเดตตำแหน่งที่เก็บข้อมูล<br />

หลังจากที่เราทดลองสร้างแอพที่สามารถบันทึกและเล่นไฟล์เสียงได้แล้วนั้น เราจะใช้งานคลาส<br />

MediaStore เพื่อจัดการตำแหน่งที่เก็บข้อมูลและอัพเดตรายการข้อมูลเพื่อให้ระบบสามารถนำไปใช้<br />

งานได้ ชุดคำสั่งที่ 6.11 จะแสดงการกำหนดแหล่งของข้อมูลเพื่อแสดงรายการของไฟล์เสียงเรียกเข้า<br />

และเสียงแจ้งตือนต่างๆ ซึ่งรายการไฟล์เหล่านี้จะไม่แสดงในแอพประเภท MP3 Player (เพราะค่า<br />

ของ IS_MUSIC มีค่าเป็น false)<br />

ชุดคำสั่งที่ 6.11 ตัวอย่างของการอัพเดตรายการข้อมูลเสียงเพื่อให้ระบบสามารถนำไปใช้งานได้<br />

//reload MediaScanner to search for media and update paths<br />

sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,<br />

Uri.parse("file://"<br />

+ Environment.getExternalStorageDirectory())));<br />

ContentValues values = new ContentValues();<br />

values.put(MediaStore.MediaColumns.DATA, myFile.getAbsolutePath());<br />

values.put(MediaStore.MediaColumns.TITLE, myFile.getName());<br />

values.put(MediaStore.MediaColumns.TIMESTAMP,<br />

System.currentTimeMillis());<br />

values.put(MediaStore.MediaColumns.MIME_TYPE,<br />

recorder.getMimeContentType());<br />

values.put(MediaStore.Audio.Media.ARTIST, SOME_ARTIST_HERE);<br />

values.put(MediaStore.Audio.Media.IS_RINGTONE, true);<br />

values.put(MediaStore.Audio.Media.IS_NOTIFICATION, true);<br />

values.put(MediaStore.Audio.Media.IS_ALARM, true);<br />

values.put(MediaStore.Audio.Media.IS_MUSIC, false);<br />

ContentResolver contentResolver = new ContentResolver();<br />

Uri base = MediaStore.Audio.INTERNAL_CONTENT_URI;<br />

Uri newUri = contentResolver.insert(base, values);<br />

String path = contentResolver.getDataFilePath(newUri);<br />

ค่าของ ContentValues จะใช้ในการประกาศคุณสมบัติพื้นฐานบางอย่างของไฟล์ เช่น<br />

TITLE, TIMESTAMP, MIME_TYPE และคำสั่ง ContentResolver จะใช้ในการสร้างรายการใน<br />

MediaStore โดยจะอัพเดตฐานข้อมูลพร้อมทั้งตำแหน่งที่เก็บไฟล์โดยอัตโนมัติ<br />

วิดีโอ<br />

เราจะใช้เฟรมเวิร์ค MediaPlayer และ MediaRecorder ในการบันทึกและเล่นไฟล์วิดีโอ<br />

วิธีการใช้งานก็คล้ายกับการบันทึกและเล่นไฟล์เสียงในตัวอย่างก่อนหน้านี้ โดยเราจะกำหนดค่าของ<br />

สิทธิ์การใช้งานในไฟล์ Manifest ดังนี้<br />


166 บทที่ 6 เทคนิคการทำางานร่วมกับมัลติมีเดีย<br />

หลังจากนั้นเราก็จะกำหนดค่าเริ่มต้นในการทำงานด้วยการกำหนดค่าที่คล้ายกับที่ใช้ในการบันทึก<br />

ไฟล์เสียงดังนี้<br />

m MediaRecorder.VideoSource:<br />

m CAMERA -- ใช้กล้องที่มากับอุปกรณ์แอนดรอยด์<br />

m MediaRecorder.OutputFormat:<br />

m THREE_GPP – ชนิดไฟล์แบบ 3GPP<br />

m MPEG_4 – ชนิดไฟล์แบบ MP4<br />

m MediaRecorder.VideoEncoder:<br />

m H263 – เข้ารหัสข้อมูลแบบ H.263<br />

m H264 – เข้ารหัสข้อมูลแบบ H.264<br />

m MPEG_4_SP -- เข้ารหัสข้อมูลแบบ MPEG4<br />

ขั้นตอนในการบันทึกวิดีโอมีดังนี้<br />

1. สร้างอินสแตนซ์ของ MediaRecorder<br />

MediaRecorder m_Recorder = new MediaRecorder();<br />

2. กำหนดตำแหน่งของแหล่งข้อมูลที่จะบันทึก ซึ่งในที่นี้ก็คือกล้องถ่ายรูปนั่นเอง<br />

m_Recorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);<br />

3. กำหนดชนิดของไฟล์เอาต์พุตและวิธีการเข้ารหัสข้อมูล<br />

m_Recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);<br />

m_Recorder.setAudioEncoder(MediaRecorder.AudioEncoder.H263);<br />

4. กำหนดตำแหน่งที่จะเก็บไฟล์ข้อมูลที่บันทึกไว้<br />

m_Recorder.setOutputFile(path);<br />

5. เริ่มต้นการบันทึกวิดีโอ<br />

m_Recorder.prepare();<br />

m_Recorder.start();<br />

สำหรับขั้นตอนในการเล่นไฟล์วิดีโอนั้น มีดังนี้<br />

1. สร้างอินสแตนซ์ของ MediaPlayer<br />

MediaPlayer m_mediaPlayer = new MediaPlayer();<br />

2. กำหนดตำแหน่งของแหล่งข้อมูลที่จะเล่น ซึ่งสามารถอ่านข้อมูลประเภท Raw ได้โดยตรง<br />

m_mediaPlayer = MediaPlayer.create(this, R.raw.my_video);<br />

สำหรับค่าอื่นๆ นอกเหนือจากนี้จะเป็นค่าที่เกี่ยวข้องกับระบบไฟล์ (ซึ่งจะต้องใช้ในคำสั่ง<br />

prepare)<br />

m_mediaPlayer.setDataSource(path);<br />

m_mediaPlayer.prepare();<br />

ในการทำงานของชุดคำสั่งนั้น เราจะต้องใช้คำสั่ง try-catch มาครอบไว้เพื่อป้องกัน<br />

ข้อผิดพลาดที่อาจจะเกิดขึ้นในกรณีที่ระบบหาแหล่งข้อมูลไม่พบ


3. เริ่มต้นเล่นไฟล์วิดีโอ<br />

m_mediaPlayer.start();<br />

4. เมื่อเล่นไฟล์วิดีโอเสร็จแล้ว ก็จะหยุดการทำงานของ MediaPlayer และยกเลิกอินสแตนซ์<br />

เพื่อคืนหน่วยความจำกลับสู่ระบบ<br />

m_mediaPlayer.stop();<br />

m_mediaPlayer.release();<br />

วิดีโอ<br />

167


168 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ


169<br />

บทที่ 7<br />

การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

อุปกรณ์แอนดรอยด์ที่ถูกผลิตและจำหน่ายในปัจจุบันนี้มีรายละเอียดของฮาร์ดแวร์ที่หลากหลาย<br />

และแตกต่างกันไปตามแต่ละผู้ผลิต รวมถึงการติดตั้งตัวตรวจจับต่างๆ เช่น กล้องถ่ายรูป ตัววัดอัตรา<br />

เร่ง ตัววัดสนามแม่เหล็ก ตัววัดความดันบรรยากาศ ตัววัดอุณหภูมิด้วย ซึ่งส่วนใหญ่จะมีติดตั้งมาให้อยู่<br />

แล้ว ส่วนการติดต่อต่างๆ เช่น ระบบโทรศัพท์ บลูทูธ และไวไฟ เราก็สามารถเขียนชุดคำสั่งเพื่อ<br />

ควบคุมการทำงานของอุปกรณ์เหล่านี้ได้ ในบทนี้จะแสดงถึงการใช้ชุดคำสั่งเพื่อควบคุมการทำงานของ<br />

ฮาร์ดแวร์ด้วยการใช้ API (Application Programming Interfaces) ภายในแอพที่เราพัฒนาขึ้น<br />

ชุดคำสั่งในบทนี้จะทำงานได้ดีบนอุปกรณ์แอนดรอยด์จริงๆ ถ้าทดสอบบนเครื่องมือจำลองระบบ<br />

ปฏิบัติการแอนดรอยด์นั้น อาจมีความผิดพลาดและคลาดเคลื่อนมากกว่า เพราะมีรายละเอียดของ<br />

ฮาร์ดแวร์บางอย่างไม่เหมือนกัน<br />

กล้องถ่ายรูป<br />

กล้องถ่ายรูปจัดเป็นคุณสมบัติพื้นฐานที่มีอยู่ในอุปกรณ์แอนดรอยด์ โดยในแง่ของการตลาดนั้น<br />

คุณสมบัติข้อนี้จะถูกใช้เป็นจุดขายมากที่สุด ซึ่งในอุปกรณ์แอนดรอยด์รุ่นหลังๆ ก็มีการพัฒนาความ<br />

สามารถของกล้องถ่ายรูปให้มีประสิทธิภาพมากขึ้น ปกติแล้วการประมวลผลรูปภาพนั้นจะทำได้หลังจาก<br />

ถ่ายรูปแล้วเท่านั้น แต่ในระบบปฏิบัติการแอนดรอยด์จะมีแอพเกี่ยวกับการถ่ายรูปบางแอพที่สามารถ<br />

ประมวลผลภาพเพื่อดูผลลัพธ์ก่อนที่จะถ่ายรูปได้ทันที<br />

วิธีเข้าถึงการทำงานของกล้องถ่ายรูปมีอยู่ 2 วิธี คือ ใช้อินเท็นต์ของระบบปฏิบัติการเพื่อแสดง<br />

หน้าจอการถ่ายรูปที่มีอยู่แล้วในระบบปฏิบัติการตามคำสั่งนี้<br />

Intent intent = new Intent(“android.media.action.IMAGE_CAPTURE”);<br />

startActivity(intent);<br />

ส่วนวิธีที่ 2 จะเป็นการสร้างอินสแตนซ์ของคลาส Camera ซึ่งมีความยืดหยุ่นในการใช้งาน<br />

มากกว่า เพราะสามารถปรับแต่งค่าต่างๆ และสร้างเลย์เอาต์ของกล้องถ่ายรูปได้เอง เวลาใช้งานคลาส<br />

นี้ เราจะต้องกำหนดสิทธิ์ในการใช้งานในไฟล์ Manifest ดังนี้<br />

<br />

จากคำสั่งข้างต้นระบบจะเปิดให้เราสามารถใช้คลาสเพื่อเข้าถึงการทำงานของกล้องถ่ายรูปได้


170 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

กรรมวิธี: การปรับแต่งการทำงานของกล้องถ่ายรูป<br />

การควบคุมการทำงานของกล้องถ่ายรูปนั้นจะเกี่ยวข้องกับคอมโพเน็นต์หลายตัวในระบบปฏิบัติการ<br />

m คลาส Camera – ใช้เพื่อเข้าถึงฮาร์ดแวร์ของกล้องถ่ายรูป<br />

m คลาส Camera.Parameters – ใช้เพื่อกำหนดคุณสมบัติต่างๆ ของกล้องถ่ายรูป เช่น<br />

ขนาดของรูปภาพ คุณภาพของรูปภาพ รูปแบบการใช้ไฟแฟลช และคำสั่งที่เกี่ยวข้องกับ<br />

ระบบระบุพิกัด (GPS)<br />

m เมธอด Camera Preview – กำหนดรูปแบบการแสดงผลของกล้องถ่ายรูปและวิดีโอแบบ<br />

พรีวิว<br />

m คลาส SurfaceView – เป็นส่วนของวิวที่แสดงในระดับล่างสุดของเลย์เอาต์ ใช้ในการ<br />

แสดงรูปที่พรีวิวจากกล้องถ่ายรูป<br />

ก่อนที่จะอธิบายถึงความสัมพันธ์ของคอมโพเน็นต์ที่กล่าวมาข้างต้น ลองดูในชุดคำสั่งที่ 7.1<br />

ก่อน ซึ่งได้แสดงเลย์เอาต์ที่มีการรวมเอา SurfaceView ไว้เพื่อใช้รับข้อมูลจากกล้องถ่ายรูป<br />

ชุดคำสั่งที่ 7.1 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

เราสามารถเพิ่มปุ่มควบคุมการทำงานของกล้องถ่ายรูปได้โดยใช้เลย์เอาต์ที่แยกการทำงานจาก<br />

เลย์เอาต์หลักดังที่แสดงในชุดคำสั่งที่ 7.2 ในชุดคำสั่งเลย์เอาต์นี้จะประกอบไปด้วยปุ่มที่อยู่ตรงด้าน<br />

ล่าง และตรงกลางของจอภาพจะเป็นส่วนที่แสดงรูปภาพ<br />

ชุดคำสั่งที่ 7.2 res/layout/cameraoverlay.xml<br />

<br />


android:layout_height="wrap_content"<br />

android:orientation="horizontal"<br />

android:gravity="center_horizontal"><br />

<br />

<br />

<br />

กล้องถ่ายรูป<br />

171<br />

แอคทิวิตี้หลักของชุดคำสั่งนี้ประกอบด้วยการทำงานหลายอย่าง ในขั้นแรกเราจะกำหนดการ<br />

ทำงานของเลย์เอาต์ก่อน ดังนี้<br />

1. กำหนดหน้าต่างให้มีลักษณะขุ่นและแสดงแบบเต็มจอ (สำหรับอินสแตนซ์ที่เราสร้างขึ้นนี้<br />

จะถูกกำหนดให้ซ่อนไตเติ้ลบาร์และแถบ Notification)<br />

2. SurfaceView ที่ได้กำหนดไว้ในเลย์เอาต์ก่อนหน้านี้ (R.id.surface) ใช้เป็นพื้นที่วาง<br />

ส่วนแสดงภาพพรีวิวของกล้องถ่ายรูป โดยที่แต่ละ SurfaceView จะมี SurfaceHolder<br />

เพื่อใช้ควบคุมการทำงาน<br />

3. กำหนด LayoutInflater เพื่อให้เลย์เอาต์ย่อยอื่นๆ (cameraoverlay.xml) สามารถ<br />

แสดงผลในเลย์เอาต์หลักได้ (main.xml)<br />

ขั้นตอนต่อไปเป็นการกำหนดการทำงานแอคทิวิตี้เพื่อใช้สั่งให้กล้องถ่ายรูปทำงาน<br />

1. เพิ่ม onClickListener ลงไปในปุ่มที่แสดงอยู่ในเลย์เอาต์ cameraoverlay และเมื่อ<br />

ปุ่มถูกกด กล้องถ่ายรูปก็จะถ่ายรูป (mCamera.takePicture())<br />

2. การทำงานของเมธอด takePicture() นั้นจะต้องเรียกใช้เมธอดที่เกี่ยวข้องก่อนการ<br />

ทำงานดังนี้<br />

m ShutterCallback() - ใช้เพื่อสร้างเอฟเฟ็กต์ต่างๆ หลังจากการถ่ายรูปแล้ว เช่น<br />

การเล่นไฟล์เสียงเพื่อแจ้งให้ผู้ใช้รู้ว่าได้ถ่ายรูปไปแล้ว<br />

m PictureCallback() - ใช้เพื่อรับข้อมูลของรูปภาพแบบ Raw ในกรณีที่<br />

ฮาร์ดแวร์มีหน่วยความจำเพียงพอก็จะใช้เมธอดนี้ได้<br />

m PictureCallback() ชุดที่ 2 - ใช้เพื่อรับข้อมูลภาพแบบบีบอัด ซึ่งจะเรียกใช้<br />

เมธอด done() เพื่อจัดเก็บรูปภาพ<br />

ขั้นตอนต่อไปจะเป็นแอคทิวิตี้ที่ใช้ในการจัดเก็บรูปภาพที่ได้ถ่ายไว้<br />

1. ข้อมูลของรูปภาพที่ถูกบีบอัดแล้วจะเก็บอยู่ในตัวแปร tempdata และใช้คำสั่ง Bitmap-<br />

Factory เพื่อถอดรหัสข้อมูลจากชนิด ByteArray ไปเป็นออบเจ็กต์ Bitmap<br />

2. ใช้คำสั่ง Media Content Provider เพื่อจัดเก็บข้อมูลบิตแมพ และส่งค่ากลับไปเป็น<br />

URL ถ้าแอคทิวิตี้หลักถูกเรียกใช้งานโดยแอคทิวิตี้อื่นแล้วละก็ ค่าของ URL นี้จะเป็นค่า<br />

ของแอคทิวิตี้ที่ถูกเรียกใช้เพื่อจัดเก็บภาพ<br />

3. เมื่อเสร็จการทำงานข้างต้นแล้ว ให้เรียกใช้คำสั่ง finish() เพื่อยกเลิกแอคทิวิตี้


172 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

ในขั้นตอนสุดท้ายจะเป็นการสร้างแอคทิวิตี้ที่ตอบสนองต่อการเปลี่ยนแปลงใน SurfaceView<br />

1. สร้างอินเตอร์เฟซ SurfaceHolder.CallBack ซึ่งจะต้องโอเวอร์ไรด์เมธอดจำนวน 3<br />

ตัวดังนี้<br />

m surfaceCreated() – จะถูกเรียกใช้เมื่อมีการสร้าง Surface ครั้งแรก<br />

m surfaceChanged() – จะถูกเรียกใช้เมื่อสร้าง Surface เรียบร้อยแล้ว หรือเมื่อ<br />

Surface มีการเปลี่ยนแปลง (เช่นเปลี่ยนแปลงรูปแบบหรือขนาด)<br />

m surfaceDestroyed() – จะถูกเรียกใช้ในระหว่างที่มีการเอา Surface ออกจาก<br />

วิวและ Surface ถูกยกเลิก<br />

2. ค่าพารามิเตอร์ของจะถูกเปลี่ยนเมื่อ Surface มีการเปลี่ยนแปลง<br />

ในชุดคำสั่งที่ 7.3 จะแสดงการทำงานของแอคทิวิตี้ทั้งหมด<br />

ชุดคำสั่งที่ 7.3 src/com/cookbook/hardware/CameraApplication.java<br />

package com.cookbook.hardware;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.graphics.Bitmap;<br />

import android.graphics.BitmapFactory;<br />

import android.graphics.PixelFormat;<br />

import android.hardware.Camera;<br />

import android.hardware.Camera.PictureCallback;<br />

import android.hardware.Camera.ShutterCallback;<br />

import android.os.Bundle;<br />

import android.provider.MediaStore.Images;<br />

import android.util.Log;<br />

import android.view.LayoutInflater;<br />

import android.view.SurfaceHolder;<br />

import android.view.SurfaceView;<br />

import android.view.View;<br />

import android.view.Window;<br />

import android.view.WindowManager;<br />

import android.view.View.OnClickListener;<br />

import android.view.ViewGroup.LayoutParams;<br />

import android.widget.Button;<br />

import android.widget.Toast;<br />

public class CameraApplication extends Activity<br />

implements SurfaceHolder.Callback {<br />

private static final String TAG = "cookbook.hardware";<br />

private LayoutInflater mInflater = null;


กล้องถ่ายรูป<br />

173<br />

Camera mCamera;<br />

byte[] tempdata;<br />

boolean mPreviewRunning = false;<br />

private SurfaceHolder mSurfaceHolder;<br />

private SurfaceView mSurfaceView;<br />

Button takepicture;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

getWindow().setFormat(PixelFormat.TRANSLUCENT);<br />

requestWindowFeature(Window.FEATURE_NO_TITLE);<br />

getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,<br />

WindowManager.LayoutParams.FLAG_FULLSCREEN);<br />

setContentView(R.layout.main);<br />

mSurfaceView = (SurfaceView)findViewById(R.id.surface);<br />

mSurfaceHolder = mSurfaceView.getHolder();<br />

mSurfaceHolder.addCallback(this);<br />

mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);<br />

}<br />

mInflater = LayoutInflater.from(this);<br />

View overView = mInflater.inflate(R.layout.cameraoverlay, null);<br />

this.addContentView(overView,<br />

new LayoutParams(LayoutParams.FILL_PARENT,<br />

LayoutParams.FILL_PARENT));<br />

takepicture = (Button) findViewById(R.id.button);<br />

takepicture.setOnClickListener(new OnClickListener(){<br />

public void onClick(View view){<br />

mCamera.takePicture(mShutterCallback,<br />

mPictureCallback, mjpeg);<br />

}<br />

});<br />

ShutterCallback mShutterCallback = new ShutterCallback(){<br />

@Override<br />

public void onShutter() {}<br />

};<br />

PictureCallback mPictureCallback = new PictureCallback() {<br />

public void onPictureTaken(byte[] data, Camera c) {}<br />

};<br />

PictureCallback mjpeg = new PictureCallback() {


174 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

};<br />

public void onPictureTaken(byte[] data, Camera c) {<br />

if(data !=null) {<br />

tempdata=data;<br />

done();<br />

}<br />

}<br />

void done() {<br />

Bitmap bm = BitmapFactory.decodeByteArray(tempdata,<br />

0, tempdata.length);<br />

String url = Images.Media.insertImage(getContentResolver(),<br />

bm, null, null);<br />

bm.recycle();<br />

Bundle bundle = new Bundle();<br />

if(url!=null) {<br />

bundle.putString("url", url);<br />

Intent mIntent = new Intent();<br />

mIntent.putExtras(bundle);<br />

setResult(RESULT_OK, mIntent);<br />

} else {<br />

Toast.makeText(this, "Picture can not be saved",<br />

Toast.LENGTH_SHORT).show();<br />

}<br />

finish();<br />

}<br />

@Override<br />

public void surfaceChanged(SurfaceHolder holder, int format,<br />

int w, int h) {<br />

Log.e(TAG, "surfaceChanged");<br />

try {<br />

if (mPreviewRunning) {<br />

mCamera.stopPreview();<br />

mPreviewRunning = false;<br />

}<br />

Camera.Parameters p = mCamera.getParameters();<br />

p.setPreviewSize(w, h);<br />

mCamera.setParameters(p);<br />

mCamera.setPreviewDisplay(holder);<br />

mCamera.startPreview();


เซ็นเซอร์ตรวจจับต่างๆ<br />

175<br />

}<br />

mPreviewRunning = true;<br />

} catch(Exception e) {<br />

Log.d("",e.toString());<br />

}<br />

@Override<br />

public void surfaceCreated(SurfaceHolder holder) {<br />

Log.e(TAG, "surfaceCreated");<br />

mCamera = Camera.open();<br />

}<br />

}<br />

@Override<br />

public void surfaceDestroyed(SurfaceHolder holder) {<br />

Log.e(TAG, "surfaceDestroyed");<br />

mCamera.stopPreview();<br />

mPreviewRunning = false;<br />

mCamera.release();<br />

mCamera=null;<br />

}<br />

การแสดงรูปพรีวิวจากกล้องถ่ายรูปนั้นยังไม่มีมาตรฐานที่แน่นอน ในอุปกรณ์แอนดรอยด์บางตัว<br />

จะแสดงผลลัพธ์แบบตะแคง ในกรณีนี้เราจะเพิ่มคำสั่งลงไปในเมธอด onCreate() ของแอคทิวิตี้<br />

CameraPreview<br />

this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);<br />

เซ็นเซอร์ตรวจจับต่างๆ<br />

ในปัจจุบันนี้การพัฒนาอุปกรณ์ประเภท MEMS (Micro-Electro-Mechanical System)<br />

มีความก้าวหน้าอย่างมาก โดยมีขนาดที่เล็กลงและใช้พลังงานต่ำ ดังนั้นจึงมีการนำอุปกรณ์เหล่านี้มา<br />

ติดตั้งในโทรศัพท์ประเภทสมาร์ทโฟนกันมากขึ้น<br />

จากที่ได้กล่าวไว้ในบทที่ 1 “ก้าวแรกกับแอนดรอยด์” จะเห็นว่าอุปกรณ์แอนดรอยด์ในแต่ละรุ่น<br />

นั้นมีการติดตั้งตัวตรวจจับที่แตกต่างกันไป แต่โดยพื้นฐานแล้วจะมีการติดตั้งอุปกรณ์ตรวจจับอยู่<br />

2 ชนิด คือ ตัวอัตราเร่งแบบ 3 แกนเพื่อตรวจสอบแรงโน้มถ่วงที่กระทำต่ออุปกรณ์ในแนวแกน x, y, z<br />

และตัววัดค่าสนามแม่เหล็กแบบ 3 แกนเพื่อใช้ตรวจสอบทิศทางของอุปกรณ์ ส่วนตัวตรวจจับอื่นๆ<br />

นอกเหนือจากนี้ก็เช่นตัวตรวจจับอุณหภูมิ, ตัวตรวจจับแสงสว่าง หรือไจโรสโคป ในตารางที่ 7.1 นี้ได้<br />

แสดงรายการของตัวตรวจจับที่สามารถทำงานร่วมกับ Android SDK เอาไว้


176 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

ตารางที่ 7.1 แสดงรายการของตัวตรวจจับที่สามารถทำงานร่วมกับ Android SDK<br />

ชนิดของตัวตรวจจับ<br />

รายละเอียด<br />

TYPE_ACCELEROMETER<br />

ตรวจวัดอัตราเร่ง หน่วยเป็น เมตร/วินาทียกกำาลัง 2<br />

TYPE_ALL<br />

เป็นค่าคงที่ที่แทนค่าของตัวตรวจจับทั้งหมด<br />

TYPE_GYROSCOPE<br />

TYPE_LIGHT<br />

TYPE_MAGNETIC_FIELD<br />

TYPE_PRESSURE<br />

TYPE_PROXIMITY<br />

TYPE_TEMPERATURE<br />

ตรวจวัดทิศทาง โดยอ้างอิงจากความเร็งเชิงมุม<br />

ตรวจวัดความเข้มแสง มีหน่วยเป็น lux<br />

ตรวจวัดอัตราความเข้มของสนามแม่เหล็ก หน่วยเป็น Micro-Tesla<br />

ตรวจวัดแรงดันบรรยากาศ<br />

ตรวจวัดระยะห่างของวัตถุกับตัวตรวจจับ หน่วยเป็นเซนติเมตร<br />

ตรวจวัดอุณหภูมิ หน่วยเป็นองศาเซลเซียส<br />

เมธอด getSensorList() ใช้เวลาแสดงรายการของตัวตรวจจับที่มีใช้ในอุปกรณ์นั้นๆ และ<br />

อีเวนต์ onSensorChanged(), onAccuracyChanged() จะใช้เพื่อตรวจดูว่าตัวตรวจจับและค่าความ<br />

แม่นยำมีการเปลี่ยนแปลงหรือไม่<br />

กรรมวิธี: การตรวจสอบทิศทางของโทรศัพท์<br />

ตัวตรวจจับที่ใช้ในหัวข้อนี้คือตัวตรวจจับอัตราเร่งจากแรงโน้มถ่วงของโลก (Accelerometer)<br />

ซึ่งมีค่า G= 9.8 เมตร/วินาทียกกำลัง 2 และตัวตรวจจับสนามแม่เหล็ก ซึ่งจะตรวจจับในช่วง H=30<br />

ไมโครเทสลา จนถึง 60 ไมโครเทสลา เราจะใช้ตัวตรวจจับทั้ง 2 นี้มาหาค่าอ้างอิงที่ใช้หาค่าการหมุน<br />

ของอุปกรณ์ในคำสั่ง getRotationMatrix()<br />

ค่าของแกนทั้ง 3 ที่อุปกรณ์จะใช้อ้างอิงมีดังนี้<br />

m แกน x – แทนระนาบด้านกว้างของจอภาพ<br />

m แกน y – แทนระนาบด้านยาวของจอภาพ<br />

m แกน z – แทนระนาบด้านแกนของจุดสัมผัสของจอภาพ<br />

ดูด้านล่างนี้เพื่อให้เข้าใจได้ง่ายขึ้น<br />

m แกน x – แทนทิศทางพื้นดินตรงจุดปัจจุบัน ตรงไปยังทิศตะวันออก<br />

m แกน y – แทนทิศทางพื้นดินตรงจุดปัจจุบัน ตรงไปยังขั้วโลกเหนือ<br />

m แกน z – แทนทิศทางจากพื้นดิน ตรงขึ้นไปบนท้องฟ้า<br />

การตรวจจับค่าของแกนทั้ง 3 จะอ้างอิงจากการวางอุปกรณ์บนพื้นที่เรียบ และหันส่วนบนของ<br />

อุปกรณ์ไปยังทิศเหนือ การวางอุปกรณ์ในตำแหน่งแบบนี้จะทำให้ตัวตรวจจับวัดค่าได้ (0,0,G) ตามแนว<br />

แกน x, y, z ตามลำดับ


177<br />

ในขณะที่อุปกรณ์หมุนหรือเอียงนั้น คำสั่ง SensorManager.getRotationMatrix() จะส่งค่า<br />

ของเมทริกซ์ขนาด 3x3 ลงในตัวแปร R[] โดยใช้ค่าที่ได้จากตัวตรวจวัด ซึ่งค่าของเมทริกซ์ขนาด 3x3<br />

จะเป็นค่าอ้างอิงที่ได้มาจากค่าอ้างอิงของสนามแม่เหล็กโลก (0,H,0)<br />

ถ้าอุปกรณ์ที่ใช้อยู่มีการเคลื่อนที่หรืออยู่ใกล้สนามแม่เหล็ก ค่าที่วัดได้อาจจะไม่ได้อ้างอิงจาก<br />

สนามแม่เหล็กโลก<br />

อีกหนึ่งวิธีที่ใช้ในการตรวจสอบการหมุนของอุปกรณ์ นั่นคือการใช้คำสั่ง SensorManager.<br />

getOrientation() ซึ่งจะส่งค่าของเมทริกซ์ R[] และค่าของทิศทาง attitude[] :<br />

m attitude[0] – จะเป็นค่ามุม Azimuth (หน่วยเป็นเรเดียน) ในแนวแกน z โดยค่าที่ได้<br />

จะอยู่ระหว่าง –PI และ PI ซึ่งค่า 0 จะแทนทิศเหนือ และค่า PI/2 จะแทนทิศตะวันออก<br />

m attitude[1] – จะเป็นค่า Pitch (หน่วยเป็นเรเดียน) ในแนวแกน x โดยค่าที่ได้จะอยู่<br />

ระหว่าง –PI และ PI ซึ่งค่า 0 จะแทนอุปกรณ์หงายหน้าขึ้น และค่า PI/2 จะแทน<br />

อุปกรณ์คว่ำหน้าลง<br />

m attitude[2] – จะเป็นค่า Roll (หน่วยเป็นเรเดียน) ในแนวแกน โดยค่าที่ได้จะอยู่<br />

ระหว่าง –PI และ PI ซึ่งค่า 0 จะแทนอุปกรณ์หันไปทางด้านซ้าย และค่า PI/2 จะแทน<br />

อุปกรณ์หันไปทางด้านขวา<br />

ตัวอย่างนี้จะแสดงข้อมูลทิศทางของอุปกรณ์ลงบนจอภาพ และใช้เลย์เอาต์ในการแสดงผลซึ่ง<br />

กำหนดไว้ในไฟล์เลย์เอาต์ตามชุดคำสั่งที่ 7.4 ดังนี้<br />

ชุดคำสั่งที่ 7.4 res/layout/main.xml<br />

เซ็นเซอร์ตรวจจับต่างๆ<br />

<br />

<br />

<br />

<br />

แอคทิวิตี้หลักที่แสดงในชุดคำสั่งที่ 7.5 ตัวตรวจจับจะส่งค่ากลับไปยัง Sensor Listener ชื่อ<br />

SensorEventListener ซึ่งค่าเหล่านี้จะเก็บลงในเมทริกซ์ และแปลงค่าดังกล่าวจากหน่วยเรเดียนไป<br />

เป็นองศาและแสดงข้อมูลที่ได้บนจอภาพ


178 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

เราสามารถกำหนดอัตราการอัพเดตข้อมูลของตัวตรวจจับได้ดังนี้<br />

m SENSOR_DELAY_FASTEST – มีความถี่ในการอัพเดตสูงสุด (8 ms–30 ms)<br />

m SENSOR_DELAY_GAME – มีความถี่ในการอัพเดตที่เหมาะสมกับแอพประเภทเกม (40 ms)<br />

m SENSOR_DELAY_NORMAL – มีความถี่ในการอัพเดตแบบปกติใช้เพื่อตรวจจับการหมุนของ<br />

อุปกรณ์ (200 ms)<br />

m SENSOR_DELAY_UI – มีความถี่ในการอัพเดตที่เหมาะสมกับการทำงานของ UI (350 ms)<br />

ชุดคำสั่งที่ 7.5 src/com/cookbook/orientation/OrientationMeasurements.java<br />

package com.cookbook.orientation;<br />

import android.app.Activity;<br />

import android.hardware.Sensor;<br />

import android.hardware.SensorEvent;<br />

import android.hardware.SensorEventListener;<br />

import android.hardware.SensorManager;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

public class OrientationMeasurements extends Activity {<br />

private SensorManager myManager = null;<br />

TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.attitude);<br />

// Set Sensor Manager<br />

myManager = (SensorManager)getSystemService(SENSOR_SERVICE);<br />

myManager.registerListener(mySensorListener,<br />

myManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),<br />

SensorManager.SENSOR_DELAY_GAME);<br />

myManager.registerListener(mySensorListener,<br />

myManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD),<br />

SensorManager.SENSOR_DELAY_GAME);<br />

}<br />

float[] mags = new float[3];<br />

float[] accels = new float[3];<br />

float[] RotationMat = new float[9];<br />

float[] InclinationMat = new float[9];<br />

float[] attitude = new float[3];<br />

final static double RAD2DEG = 180/Math.PI;<br />

private final SensorEventListener mySensorListener<br />

= new SensorEventListener() {<br />

@Override


public void onSensorChanged(SensorEvent event)<br />

{<br />

int type = event.sensor.getType();<br />

เซ็นเซอร์ตรวจจับต่างๆ<br />

179<br />

if(type == Sensor.TYPE_MAGNETIC_FIELD) {<br />

mags = event.values;<br />

}<br />

if(type == Sensor.TYPE_ACCELEROMETER) {<br />

accels = event.values;<br />

}<br />

}<br />

SensorManager.getRotationMatrix(RotationMat,<br />

InclinationMat, accels, mags);<br />

SensorManager.getOrientation(RotationMat, attitude);<br />

tv.setText("Azimuth, Pitch, Roll:\n"<br />

+ attitude[0]*RAD2DEG + "\n"<br />

+ attitude[1]*RAD2DEG + "\n"<br />

+ attitude[2]*RAD2DEG);<br />

}<br />

};<br />

public void onAccuracyChanged(Sensor sensor, int accuracy) {}<br />

เพื่อประสิทธิภาพในการทำงานของตัวตรวจจับ เราจะหลีกเลี่ยงการใส่ชุดคำสั่งที่มีเกี่ยวข้องกับ<br />

การคำนวณลงในเมธอด onSensorChanged() เพื่อป้องกันอาการหน่วงในขณะที่ทำงาน และเราจะใช้<br />

SensorEvent ในการเรียกใช้ข้อมูลที่ได้จากตัวตรวจจับ และเพื่อให้ได้รับข้อมูลที่ถูกต้องมากที่สุดใน<br />

ขณะนั้น เราจะใช้คำสั่ง clone() เพื่อเก็บข้อมูลจากเมทริกซ์ดังนี้<br />

accels = event.values.clone();<br />

คำสั่งข้างต้นจะใช้เพื่อให้แน่ใจว่าค่าที่เราสำเนาไปใช้งานนั้น จะไม่ถูกเปลี่ยนแปลงโดยการ<br />

ทำงานของตัวตรวจจับในขณะนั้น<br />

กรรมวิธี: การใช้งานเซ็นเซอร์ตรวจจับแสงสว่างและอุณหภูมิ<br />

การใช้เซ็นเซอร์ตรวจวัดอุณหภูมินั้นจะใช้ในแง่ของการวัดอุณหภูมิของวงจรภายในอุปกรณ์<br />

เพื่อใช้ในการปรับแต่งค่าต่างๆ ให้ใกล้เคียงกับความเป็นจริง เซ็นเซอร์ตรวจจับแสงสว่างจะทำการ<br />

ตรวจวัดความเข้มแสงภายนอกเพื่อนำมาใช้ปรับแสงสว่างของจอภาพให้เหมาะสม<br />

ในอุปกรณ์แอนดรอยด์บางรุ่นจะไม่มีการติดตั้งตัวตรวจจับทั้ง 2 ตัวนี้ ดังนั้นเราจึงต้องมีชุด<br />

คำสั่งเพิ่มเติมเพื่อจัดการข้อผิดพลาดดังกล่าว ในชุดคำสั่งที่ 7.6 นั้นเป็นการแสดงการอ่านข้อมูลจาก<br />

เซ็นเซอร์ทั้ง 2


180 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

ชุดคำสั่งที่ 7.6 ตัวอย่างของการอ่านข้อมูลจากตัวตรวจจับแสงสว่างและอุณหภูมิ<br />

private final SensorEventListener mTListener<br />

= new SensorEventListener(){<br />

@Override<br />

public void onAccuracyChanged(Sensor sensor, int accuracy) {}<br />

@Override<br />

public void onSensorChanged(SensorEvent event) {<br />

Log.v("test Temperature",<br />

"onSensorChanged:"+event.sensor.getName());<br />

if(event.sensor.getType()==Sensor.TYPE_TEMPERATURE){<br />

tv2.setText("Temperature:"+event.values[0]);<br />

}<br />

}<br />

};<br />

private final SensorEventListener mLListener<br />

= new SensorEventListener(){<br />

@Override<br />

public void onAccuracyChanged(Sensor sensor, int accuracy) {}<br />

@Override<br />

public void onSensorChanged(SensorEvent event) {<br />

Log.v("test Light",<br />

"onSensorChanged:"+event.sensor.getName());<br />

if(event.sensor.getType()==Sensor.TYPE_LIGHT){<br />

tv3.setText("Light:"+event.values[0]);<br />

}<br />

}<br />

};<br />

sensorManager.registerListener(mTListener, sensorManager<br />

.getDefaultSensor(Sensor.TYPE_TEMPERATURE),<br />

SensorManager.SENSOR_DELAY_FASTEST);<br />

sensorManager.registerListener(mLListener, sensorManager<br />

.getDefaultSensor(Sensor.TYPE_LIGHT),<br />

SensorManager.SENSOR_DELAY_FASTEST);<br />

โทรศัพท์<br />

การใช้งานระบบโทรศัพท์ในแอนดรอยด์นั้น ในชุดพัฒนาแอพของแอนดรอยด์จะมี API ที่ใช้ใน<br />

การทำงานของระบบโทรศัพท์ เช่น การกำหนดค่าเครือข่ายโทรศัพท์ การจัดการสมุดโทรศัพท์ เป็นต้น


โทรศัพท์<br />

กรรมวิธี: การจัดการโทรศัพท์<br />

เราจะใช้งานคลาส TelephonyManager ซึ่งเป็นเซอร์วิสหนึ่งของระบบปฏิบัติการแอนดรอยด์<br />

มาใช้ในการเข้าถึงข้อมูลต่างๆ และการทำงานที่เกี่ยวข้องกับระบบโทรศัพท์ โดยการเข้าถึงการทำงาน<br />

เหล่านี้ เราจะต้องกำหนดสิทธิ์การใช้งานไว้ในไฟล์ Manifest ด้วย<br />

181<br />

<br />

ชุดคำสั่งที่ 7.7 แสดงการทำงานของแอคทิวิตี้หลักดังนี้<br />

ชุดคำสั่งที่ 7.7 src/com/cookbook/hardware.telephony/TelephonyApp.java<br />

package com.cookbook.hardware.telephony;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.telephony.TelephonyManager;<br />

import android.widget.TextView;<br />

public class TelephonyApp extends Activity {<br />

TextView tv1;<br />

TelephonyManager telManager;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv1 =(TextView) findViewById(R.id.tv1);<br />

telManager = (TelephonyManager)<br />

getSystemService(TELEPHONY_SERVICE);<br />

StringBuilder sb = new StringBuilder();<br />

sb.append("deviceid:")<br />

.append(telManager.getDeviceId()).append("\n");<br />

sb.append("device Software Ver:")<br />

.append(telManager.getDeviceSoftwareVersion()).append("\n");<br />

sb.append("Line number:")<br />

.append(telManager.getLine1Number()).append("\n");<br />

sb.append("Network Country ISO:")<br />

.append(telManager.getNetworkCountryIso()).append("\n");<br />

sb.append("Network Operator:")<br />

.append(telManager.getNetworkOperator()).append("\n");<br />

sb.append("Network Operator Name:")<br />

.append(telManager.getNetworkOperatorName()).append("\n");<br />

sb.append("Sim Country ISO:")<br />

.append(telManager.getSimCountryIso()).append("\n");<br />

sb.append("Sim Operator:")<br />

.append(telManager.getSimOperator()).append("\n");<br />

sb.append("Sim Operator Name:")<br />

.append(telManager.getSimOperatorName()).append("\n");<br />

sb.append("Sim Serial Number:")<br />

.append(telManager.getSimSerialNumber()).append("\n");


182 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

}<br />

}<br />

sb.append("Subscriber Id:")<br />

.append(telManager.getSubscriberId()).append("\n");<br />

sb.append("Voice Mail Alpha Tag:")<br />

.append(telManager.getVoiceMailAlphaTag()).append("\n");<br />

sb.append("Voice Mail Number:")<br />

.append(telManager.getVoiceMailNumber()).append("\n");<br />

tv1.setText(sb.toString());<br />

ชุดคำสั่งที่ 7.8 แสดงเลย์เอาต์ของจอภาพที่ทำงานร่วมกับระบบโทรศัพท์<br />

ชุดคำสั่งที่ 7.8 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

รูปที่ 7.1 ผลลัพธ์ที่ได้จากการใช้งานคลาส TelephonyManager


โทรศัพท์<br />

183<br />

กรรมวิธี: การอ่านค่าสถานะของโทรศัพท์<br />

คลาส PhoneStateListener จะใช้ในการแสดงข้อมูลของสถานะต่างๆ ของระบบโทรศัพท์<br />

รวมทั้งการทำงานของเครือข่ายสัญญาณ ความแรงของสัญญาณที่ได้รับ และข้อความวอยซ์เมล์ต่างๆ<br />

ซึ่งค่าบางค่าจำเป็นต้องใช้สิทธิ์ในการทำงาน ดังแสดงในตารางที่ 7.2<br />

ตารางที่ 7.2 แสดงอีเวนต์ที่เกิดขึ้นในระบบโทรศัพท์และระดับสิทธิ์ที่ต้องใช้ในการเข้าถึงข้อมูล<br />

สถานะของระบบโทรศัพท์ รายละเอียด สิทธิ์ที่ใช้งานได้<br />

LISTEN_CALL_FORWARDING_INDI-<br />

CATOR<br />

สถานะการโอนสาย<br />

READ_PHONE_STATE<br />

LISTEN_CALL_STATE สถานะของการเรียกสาย READ_PHONE_STATE<br />

LISTEN_CELL_LOCATION สถานะการเปลี่ยนเซลล์ไซต์ ACCESS_COARSE_LOCATION<br />

LISTEN_DATA_ACTIVITY การเปลี่ยนทิศทางการส่งข้อมูล READ_PHONE_STATE<br />

LISTEN_DATA_CONNECTION_STATE สถานะการเชื่อมต่อข้อมูล None<br />

LISTEN_MESSAGE_WAITING_<br />

INDICATOR<br />

สถานะของข้อความเข้า<br />

LISTEN_NONE ยกเลิกการทำางานของ Listener None<br />

LISTEN_SERVICE_STATE สถานะของเครือข่าย None<br />

LISTEN_SIGNAL_STRENGTHS สถานะความแรงของสัญญาณ None<br />

READ_PHONE_STATE<br />

สำหรับตัวอย่างนี้ เราจะทำการรอสายเรียกเข้า โดยใช้คลาส TelephonyManager มาตรวจจับ<br />

อีเวนต์ PhoneStateListener.LISTEN_CALL_STATE ซึ่งสถานะของการเรียกเข้ามีดังนี้<br />

m CALL_STATE_IDLE – อุปกรณ์ยังไม่ถูกใช้งาน<br />

m CALL_STATE_RINGING – อุปกรณ์กำลังส่งเสียงสายเรียกเข้า<br />

m CALL_STATE_OFFHOOK – อุปกรณ์กำลังรับสายเรียกเข้า


184 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

ในส่วนนี้เราจะใช้ Logcat (จะกล่าวถึงในบทที่ 12 “การตรวจสอบการทำงานของแอพ”) ในการ<br />

แสดงสถานะการทำงานของอุปกรณ์กัน<br />

ชุดคำสั่งที่ 7.9 แสดงแอคทิวิตี้หลักที่จะสร้างอินสแตนซ์ของ PhoneStateListener ซึ่งจะ<br />

เรียกใช้เมธอด onCallStateChanged เพื่อตรวจจับอีเวนต์ของการเปลี่ยนสถานะสายเรียกเข้า<br />

ส่วนเมธอดอื่นๆ ที่สามารถเรียกใช้ได้ก็คือ onCallForwardingIndicator(), onCellLocation-<br />

Changed() และ onDataActivity()<br />

ชุดคำสั่งที่ 7.9 src/com/cookbook/hardware.telephony/HardwareTelephony.java<br />

package com.cookbook.hardware.telephony;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.telephony.PhoneStateListener;<br />

import android.telephony.TelephonyManager;<br />

import android.util.Log;<br />

import android.widget.TextView;<br />

public class HardwareTelephony extends Activity {<br />

TextView tv1;<br />

TelephonyManager telManager;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv1 =(TextView) findViewById(R.id.tv1);<br />

telManager = (TelephonyManager)<br />

getSystemService(TELEPHONY_SERVICE);<br />

}<br />

telManager.listen(new TelListener(),<br />

PhoneStateListener.LISTEN_CALL_STATE);<br />

private class TelListener extends PhoneStateListener {<br />

public void onCallStateChanged(int state, String incomingNumber) {<br />

super.onCallStateChanged(state, incomingNumber);


บลูทูธ<br />

185<br />

}<br />

}<br />

}<br />

Log.v("Phone State", "state:"+state);<br />

switch (state) {<br />

case TelephonyManager.CALL_STATE_IDLE:<br />

Log.v("Phone State",<br />

"incomingNumber:"+incomingNumber+" ended");<br />

break;<br />

case TelephonyManager.CALL_STATE_OFFHOOK:<br />

Log.v("Phone State",<br />

"incomingNumber:"+incomingNumber+" picked up");<br />

break;<br />

case TelephonyManager.CALL_STATE_RINGING:<br />

Log.v("Phone State",<br />

"incomingNumber:"+incomingNumber+" received");<br />

break;<br />

default:<br />

break;<br />

}<br />

กรรมวิธี: การโทรออกจากเบอร์ที่กำหนด<br />

การทำให้แอพที่เราสร้างขึ้นสามารถโทรออกได้ เราจะต้องกำหนดสิทธิ์การใช้งานในไฟล์<br />

Manifest ดังนี้<br />

<br />

เราจะใช้อินเท็นต์ ACTION_CALL หรือ ACTION_DIALER ซึ่งเมื่อใช้อินเท็นต์นี้บนจอก็จะแสดง<br />

หน้าจอของการหมุนโทรศัพท์ขึ้นมา พร้อมทั้งแสดงเบอร์โทรศัพท์ที่ต้องการโทรออกด้วย ดังคำสั่งนี้<br />

startActivity(new Intent(Intent.ACTION_CALL,<br />

Uri.parse(“tel:15102345678”)));<br />

แต่ถ้าไม่อยากให้จอภาพแสดงหน้าจอการหมุนโทรศัพท์ขึ้นมา ให้ใช้คำสั่งนี้<br />

startActivity(new Intent(Intent.ACTION_DIAL,<br />

Uri.parse(“tel:15102345678”)));<br />

บลูทูธ<br />

บลูทูธเป็นอุปกรณ์สื่อสารแบบไร้สายประเภทหนึ่งที่สร้างขึ้นตามมาตรฐาน IEEE 802.15.1<br />

มีลักษณะเป็นโปรโตคอลแบบเปิดที่ใช้ในการแลกเปลี่ยนข้อมูลระหว่างอุปกรณ์ในระยะทางที่ไม่ไกลมาก<br />

นัก เช่น ระหว่างเครื่องโทรศัพท์ และอุปกรณ์ประเภทหูฟัง


186 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

การใช้งานบลูทูธมีลำดับการทำงานดังนี้<br />

1. เปิดการใช้งานบลูทูธ<br />

2. จับคู่กับอุปกรณ์ที่ต้องการจะเชื่อมต่อ<br />

3. เชื่อมต่ออุปกรณ์ทั้ง 2<br />

4. เริ่มต้นการรับส่งข้อมูล<br />

การใช้งานบลูทูธนั้น เราจะต้องกำหนดสิทธิ์ในการใช้งานเพื่อให้สามารถรับส่งข้อมูลได้ และต้อง<br />

กำหนดค่าของ BLUETOOTH_ADMIN ให้มีสิทธิ์ในการค้นหาอุปกรณ์อื่น รวมทั้งการแสดงตัวเองใน<br />

เครือข่ายด้วย ซึ่งจะกำหนดค่าในไฟล์ Manifest ดังนี้<br />

<br />

<br />

การใช้งาน API เกี่ยวกับบลูทูธของแอนดอรยด์นั้นจะเรียกใช้จากแพ็คเกจ android.bluetooth<br />

ซึ่งมีคลาสหลักที่ใช้ในการทำงานอยู่ 5 คลาสดังนี้<br />

m BluetoothAdapter – ใช้ค้นหาอุปกรณ์บลูทูธและจับคู่กัน<br />

m BluetoothClass – ใช้แสดงรายละเอียดของอุปกรณ์บลูทูธที่ใช้อยู่<br />

m BluetoothDevice – ใช้ทำงานร่วมกับอุปกรณ์บลูทูธที่เชื่อมต่อไว้<br />

m BluetoothSocket – ใช้จัดการข้อมูลที่รับส่งระหว่างอุปกรณ์บลูทูธ<br />

m BluetoothServerSocket – ใช้เปิดช่องสัญญาณเพื่อตรวจจับข้อมูลที่ส่งมาจาก<br />

อุปกรณ์บลูทูธอื่นๆ<br />

ในหัวข้อถัดไปเราจะมาดูรายละเอียดการทำงานของขั้นตอนต่างๆ กัน<br />

กรรมวิธี: การเปิดการทำงานของบลูทูธ<br />

เราจะใช้คลาส Bluetoothadapter เพื่อสั่งให้บลูทูธเริ่มทำงาน ซึ่งเมธอด getDefault<br />

Adapter() จะรับข้อมูลเกี่ยวกับรายละเอียดการเชื่อมต่อของบลูทูธ ถ้าเมธอดมีการส่งค่ากลับเป็น<br />

null นั่นแสดงว่าอุปกรณ์นั้นไม่รองรับการใช้งานบลูทูธ<br />

BluetoothAdapter myBluetooth = BluetoothAdapter.getDefaultAdapter();<br />

การใช้คลาส BluetoothAdapter เพื่อเริ่มใช้งานบลูทูธนั้น ถ้าอุปกรณ์รองรับบลูทธแต่ยังไม่ได้<br />

เปิดใช้งาน ก็จะต้องใช้คำสั่ง ACTION_REQUEST_ENABLE เพื่อเปิดการใช้งานบลูทูธก่อน ดังนี้


บลูทูธ<br />

187<br />

if(!myBluetooth.isEnabled()) {<br />

Intent enableIntent = new Intent(BluetoothAdapter<br />

.ACTION_REQUEST_ENABLE);<br />

startActivity(enableIntent);<br />

}<br />

กรรมวิธี: การค้นหาอุปกรณ์บลูทูธ<br />

หลังจากที่เปิดการใช้งานบลูทูธแล้ว วิธีที่จะค้นหาหรือจับคู่อุปกรณ์บลูทูธนั้น เราจะใช้เมธอด<br />

startdiscovery() ของคลาส BluetoothAdapter ในการค้นหาอุปกรณ์ เวลาใช้เมธอดนี้เราต้อง<br />

ประกาศใช้งาน BroadcastReceiver ก่อนเพื่อตรวจจับอีเวนต์ ACTION_FOUND ซึ่งแสดงว่ามีการค้น<br />

พบอุปกรณ์บลูทูธอื่นๆ ที่อยู่ใกล้เคียง การทำงานนี้จะแสดงอยู่ในชุดคำสั่งที่ 7.10<br />

ชุดคำสั่งที่ 7.10 ตัวอย่างชุดคำสั่งที่ใช้ในการค้นหาอุปกรณ์บลูทูธ<br />

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {<br />

public void onReceive(Context context, Intent intent) {<br />

String action = intent.getAction();<br />

// When discovery finds a device<br />

if (BluetoothDevice.ACTION_FOUND.equals(action)) {<br />

// Get the BluetoothDevice object from the Intent<br />

BluetoothDevice device = intent.getParcelableExtra(<br />

BluetoothDevice.EXTRA_DEVICE);<br />

Log.v("BlueTooth Testing",device.getName() + "\n"<br />

+ device.getAddress());<br />

}<br />

}<br />

};<br />

IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);<br />

registerReceiver(mReceiver, filter);<br />

myBluetooth.startDiscovery();<br />

การตรวจจับอีเวนต์นั้นจะมีการตรวจหาอีเวนต์ ACTION_DISCOVERY_STARTED และ ACTION_<br />

DISCOVERY_FINISHED เพื่อแสดงสถานะของการเชื่อมต่อว่าเริ่มต้นและสิ้นสุดเมื่อใด<br />

สำหรับการเปิดให้อุปกรณ์ที่กำลังใช้งานอยู่สามารถถูกค้นพบโดยอุปกรณ์บลูทูธอื่นๆ นั้น เราจะใช้<br />

อินเท็นต์ ACTION_REQUEST_DISCOVERABLE เพื่อแสดงหน้าจอในส่วนของการกำหนดสถานะเพื่อให้<br />

อุปกรณ์สามารถถูกค้นหาโดยอุปกรณ์บลูทูธอื่นๆ ได้<br />

Intent discoverableIntent<br />

= new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);<br />

startActivity(discoverableIntent);


188 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

กรรมวิธี: การจับคู่ระหว่างอุปกรณ์บลูทูธ<br />

การจับคู่อุปกรณ์บลูทูธเป็นการทำเพื่อให้ระบบรับรู้ว่าอุปกรณ์ดังกล่าวได้ค้นพบและเชื่อมต่อกัน<br />

แล้ว ซึ่งในการเชื่อมต่อครั้งถัดไปก็ไม่จำเป็นต้องค้นหาอุปกรณ์กันอีก ในการจับคู่อุปกรณ์บลูทูธนั้น<br />

อุปกรณ์ตัวที่หนึ่งจะทำหน้าที่เป็นเซิร์ฟเวอร์ และอุปกรณ์ที่เหลือจะเป็นลูกข่าย โดยเราจะใช้เมธอด<br />

getBondedDevices() ของคลาส BluetoothAdapter มาจัดการเรื่องนี้<br />

Set pairedDevices = mBluetoothAdapter.getBondedDevices();<br />

กรรมวิธี: การเปิดบลูทูธซ็อกเก็ต<br />

การเปิดการเชื่อมต่อเพื่อรับส่งข้อมูลระหว่างอุปกรณ์อื่นๆ นั้นจำเป็นต้องเปิดการทำงานของ<br />

ซ็อกเก็ตทั้งฝั่งเซิร์ฟเวอร์และฝั่งลูกข่าย หลังจากที่อุปกรณ์ทั้ง 2 เชื่อมต่อและจับคู่กันเรียบร้อยแล้ว<br />

ก็จะเปิดซ็อกเก็ตเพื่อใช้โปรโตคอล RFCOMM ซึ่งการเปิดใช้งานซ็อกเก็ตนั้นจะเปิดได้ทั้ง 2 ฝ่าย คือ<br />

สร้างซ็อกเก็ตเมื่อเซิร์ฟเวอร์ได้รับการร้องขอเพื่อเชื่อมต่อ หรือสร้างซ็อกเก็ตเมื่อลูกข่ายได้รับสัญญาณ<br />

จากเซิร์ฟเวอร์<br />

ในฝั่งผู้ใช้บริการหรือเซิร์ฟเวอร์นั้น เราจะใช้คำสั่งคล้ายๆ กับการทำงานของโปรแกรมประเภท<br />

Client-Server โดยทั่วไป (ที่ใช้โปรโตคอล TCP/IP) และอินเตอร์เฟซ BluetoothServerSocket<br />

จะใช้ในการสร้างพอร์ตเพื่อรับส่งข้อมูล หลังจากที่การเชื่อมต่อสมบูรณ์แล้ว BluetoothSocket ก็จะ<br />

ส่งค่าสถานะกลับมา และใช้ BluetoothSocket ในการจัดการการเชื่อมต่อ<br />

คำสั่ง BluetoothServerSocket จะถูกเรียกใช้งานได้จากอินสแตนซ์ BluetoothAdapter<br />

ด้วยการใช้เมธอด listenUsingRfcommWithServiceRecord() หลังจากการสร้างซ็อกเก็ตแล้ว<br />

เมธอด accept() ก็จะส่งค่ากลับเพื่อแสดงสถานะพร้อมใช้งาน และเมื่อต้องการยกเลิกการใช้งาน<br />

ซ็อกเก็ต ก็ให้ปิดการทำงานด้วยคำสั่ง close()เพื่อให้อุปกรณ์บลูทูธอื่นๆ สามารถทำงานได้ เนื่องจาก<br />

โปรโตคอล RFCOMM จะรองรับการเชื่อมต่ออุปกรณ์และใช้งานพร้อมกันได้เพียงอุปกรณ์เดียว<br />

ขั้นตอนในการเปิดใช้งานซ็อกเก็ตมีดังนี้<br />

BluetoothServerSocket myServerSocket<br />

= myBluetoothAdapter.listenUsingRfcommWithServiceRecord(name, uuid);<br />

myServerSocket.accept();<br />

myServerSocket.close();<br />

คำสั่ง accept() นั้นจะปิดกั้นการทำงานของการเรียกสายเข้า ดังนั้นเราจึงไม่ควรนำคำสั่งนี้มา<br />

ใช้ในเธรดหลักของแอพ วิธีแก้ไขปัญหานี้คือใช้คำสั่ง accept() ในเธรดย่อยเท่านั้น ดังแสดงในชุด<br />

คำสั่งที่ 7.11


ชุดคำสั่งที่ 7.11 ตัวอย่างของการสร้างบลูทูธซ็อกเก็ต<br />

บลูทูธ<br />

189<br />

private class AcceptThread extends Thread {<br />

private final BluetoothServerSocket mmServerSocket;<br />

public AcceptThread() {<br />

// Use a temporary object that is later assigned<br />

// to mmServerSocket, because mmServerSocket is final<br />

BluetoothServerSocket tmp = null;<br />

try {<br />

// MY_UUID is the app’s UUID string, also used by the client<br />

tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME,MY_UUID);<br />

} catch (IOException e) { }<br />

mmServerSocket = tmp;<br />

}<br />

public void run() {<br />

BluetoothSocket socket = null;<br />

// Keep listening until exception occurs or a socket is returned<br />

while (true) {<br />

try {<br />

socket = mmServerSocket.accept();<br />

} catch (IOException e) {<br />

break;<br />

}<br />

// If a connection was accepted<br />

if (socket != null) {<br />

// Do work to manage the connection (in a separate thread)<br />

manageConnectedSocket(socket);<br />

mmServerSocket.close();<br />

break;<br />

}<br />

}<br />

}<br />

}<br />

/** Will cancel the listening socket, and cause thread to finish */<br />

public void cancel() {<br />

try {<br />

mmServerSocket.close();<br />

} catch (IOException e) { }<br />

}<br />

การควบคุมการทำงานของบลูทูธในเครื่องลูกข่ายนั้น ซ็อกเก็ตจะต้องสร้างมาจากเครื่องลูกข่าย<br />

และส่งมายังเซิร์ฟเวอร์ แล้วเซิร์ฟเวอร์จะรับข้อมูลซ็อกเก็ตนั้นด้วยเมธอด createRfcommSocketTo<br />

ServiceRecord(UUID) ของ BluetoothDevice


190 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

ค่าของ UUID จะใช้ใน listenUsingRfcommWithServiceRecord และหลังจากที่ได้รับ<br />

ซ็อกเก็ตแล้วก็จะใช้คำสั่ง connect() เพื่อเชื่อมต่ออุปกรณ์ ซึ่งคำสั่งนี้จะต้องใช้งานในเธรดย่อย<br />

เช่นกัน ดังแสดงในชุดคำสั่งที่ 7.12 ดังนี้<br />

ชุดคำสั่งที่ 7.12 ตัวอย่างของการเชื่อมต่อบลทูธซ็อกเก็ต<br />

private class ConnectThread extends Thread {<br />

private final BluetoothSocket mmSocket;<br />

private final BluetoothDevice mmDevice;<br />

public ConnectThread(BluetoothDevice device) {<br />

// Use a temporary object that is later assigned to mmSocket,<br />

// because mmSocket is final<br />

BluetoothSocket tmp = null;<br />

mmDevice = device;<br />

}<br />

// Get a BluetoothSocket to connect with the given BluetoothDevice<br />

try {<br />

// MY_UUID is the app’s UUID string, also used by the server code<br />

tmp = device.createRfcommSocketToServiceRecord(MY_UUID);<br />

} catch (IOException e) { }<br />

mmSocket = tmp;<br />

public void run() {<br />

// Cancel discovery because it will slow down the connection<br />

mAdapter.cancelDiscovery();<br />

try {<br />

// Connect the device through the socket. This will block<br />

// until it succeeds or throws an exception<br />

mmSocket.connect();<br />

} catch (IOException connectException) {<br />

// Unable to connect; close the socket and get out<br />

try {<br />

mmSocket.close();<br />

} catch (IOException closeException) { }<br />

return;<br />

}<br />

}<br />

// Do work to manage the connection (in a separate thread)<br />

manageConnectedSocket(mmSocket);


บลูทูธ<br />

191<br />

}<br />

/** Will cancel an in-progress connection, and close the socket */<br />

public void cancel() {<br />

try {<br />

mmSocket.close();<br />

} catch (IOException e) { }<br />

}<br />

หลังจากที่การเชื่อมต่อเสร็จสิ้นแล้ว ให้ใช้คำสั่ง InputStream หรือ OutputStream เพื่อรับ<br />

ส่งข้อมูลระหว่างอุปกรณ์บลูทูธ<br />

กรรมวิธี: การใช้งานระบบสั่น<br />

ระบบสั่นนั้นเป็นคุณสมบัติพื้นฐานของโทรศัพท์ทั่วไป การที่จะควบคุมการทำงานของระบบสั่น<br />

เราจะต้องกำหนดสิทธิ์ในการใช้งานในไฟล์ Manifest ดังนี้<br />

<br />

การใช้งานระบบสั่นในระบบปฏิบัติการแอนดรอยด์นั้นจะหมือนกับการใช้งานเซอร์วิสอื่นๆ ใน<br />

แอนดรอยด์ โดยเราจะใช้คลาส Vibrator ในการทำงานนี้<br />

Vibrator myVib = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);<br />

คำสั่ง vibrate() จะสั่งให้ใช้งานระบบสั่น ด้วยระยะเวลา 3 วินาที ดังนี้<br />

myVib.vibrate(3000); //vibrate for 3 seconds<br />

ถ้าต้องการยกเลิกการทำงานของระบบสั่นก่อนระยะเวลา 3 วินาที ก็ให้ใช้คำสั่ง cancel() เพื่อ<br />

หยุดการทำงาน<br />

myVib.cancel(); //cancel the vibration<br />

เราสามารถกำหนดรูปแบบของการสั่งได้ด้วยการใช้คำสั่ง pattern ดังนี้<br />

long[] pattern = {2000,1000,5000};<br />

myVib.vibrate(pattern,1);<br />

จากคำสั่งข้างต้นจะเป็นการสั่งให้ระบบสั่นหยุดทำงาน 2 วินาที สั่น 1 วินาที และหยุดทำงาน 5<br />

วินาที เป็นเช่นนี้ไปเรื่อยๆ ในคำสั่งบรรทัดที่ 2 นั้นจะเป็นการสั่งให้อุปกรณ์เริ่มทำงานตามรูปแบบที่<br />

กำหนดไว้ ซึ่งเราสามารถกำหนดค่าให้เป็น -1 เพื่อสั่งให้อุปกรณ์ทำงานเพียงครั้งเดียว ไม่ต้องทำงาน<br />

ซ้ำได้<br />

กรรมวิธี: การติดต่อระบบเครือข่ายแบบไร้สาย<br />

มีแอพอยู่หลายประเภทที่ต้องใช้การเชื่อมต่อกับเครือข่ายในระหว่างที่แอพกำลังทำงาน ระบบ<br />

ปฏิบัติการแอนดรอยด์มีไลบรารีที่ใช้ในการเชื่อมต่อกับเครือข่ายแบบไร้สาย โดยเราจะใช้อินเท็นต์ที่<br />

เกี่ยวข้องมาจัดการการเชื่อมต่อนี้


192 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ<br />

เราจะใช้คลาส ConnectivityManager ในการตรวจสอบสถานะการเชื่อมต่อเครือข่าย<br />

หรือกำหนดเครือข่ายที่จะเชื่อมต่อ รวมทั้งจัดการข้อผิดพลาดในกรณีที่การเชื่อมต่อล้มเหลว ดังนี้<br />

ConnectivityManager myNetworkManager<br />

= (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);<br />

การใช้งานคลาส ConnectivityManager นั้น เราจะต้องกำหนดสิทธิ์ในการใช้งานลงในไฟล์<br />

Manifest ดังนี้<br />

<br />

คลาส ConnectivityManager จะมีเมธอด getNetworkInfo(() และ getActiveNetworkInfo()<br />

เพื่อใช้แสดงค่าการเชื่อมต่อเครือข่ายในช่วงเวลานั้น แต่วิธีการที่เหมาะสมในการตรวจ<br />

สอบสถานะของเครือข่ายนั้น เราจะใช้การสร้าง Broadcast Receiver มาทำงานนี้แทน ดังนี้<br />

private BroadcastReceiver mNetworkReceiver = new BroadcastReceiver(){<br />

public void onReceive(Context c, Intent i){<br />

Bundle b = i.getExtras();<br />

NetworkInfo ni = (NetworkInfo)<br />

b.get(ConnectivityManager.EXTRA_NETWORK_INFO);<br />

if(ni.isConnected()){<br />

//do the operation<br />

}else{<br />

//announce the user the network problem<br />

}<br />

}<br />

};<br />

หลังจากที่สร้าง Broadcast Receiver แล้ว ก็จะประกาศการใช้งานดังนี้<br />

this.registerReceiver(mNetworkReceiver,<br />

new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));<br />

ค่าของ mNetworkReceiver เป็นค่าที่ได้มาจาก ConnectivityManager.EXTRA_NETWORK_<br />

INFO ของคลาส NetworkInfo ซึ่งค่าเหล่านี้จะมีรายละเอียดอื่นอีก ดังแสดงไว้ในตารางที่ 7.3 ดังนี้


ตารางที่ 7.3 ตัวอย่างข้อมูลที่ได้จาก Connectivity Manager<br />

บลูทูธ<br />

193<br />

ชนิดของข้อมูล<br />

EXTRA_EXTRA_INFO<br />

EXTRA_IS_FAILOVER<br />

EXTRA_NETWORK_INFO<br />

EXTRA_NO_CONNECTIVITY<br />

รายละเอียด<br />

ข้อมูลสถานะการเชื่อมต่อเครือข่าย<br />

ข้อมูลการเชื่อมต่อเครือข่ายรองรับการทำางานแบบ FailOver<br />

ส่งค่ากลับเป็นออบเจ็กต์ NetworkInfo<br />

แสดงสถานะไม่สามารถเชื่อมต่อเครือข่ายได้<br />

EXTRA_OTHER_NETWORK_INFO ส่งค่ากลับเป็นออบเจ็กต์ NetworkInfo ซึ่งแสดงข้อมูลของเครือข่ายที่รองรับการ<br />

ทำางานแบบ FailOver<br />

EXTRA_REASON<br />

แสดงสาเหตุของการเชื่อมต่อล้มเหลว<br />

เราสามารถใช้คำสั่ง setNetworkPreference() ของคลาส ConnectivityManager ในการ<br />

เลือกเครือข่ายไร้สายได้ ซึ่งในการเปลี่ยนเครือข่ายนั้น เราจะต้องกำหนดสิทธิ์ในไฟล์ Manifest ด้วย<br />

ดังนี้<br />


194 บทที่ 7 การติดต่อกับฮาร์ดแวร์ต่างๆ


195<br />

บทที่ 8<br />

เครือข่าย<br />

แอพที่มีการเชื่อมต่อเครือข่ายมีจุดเด่นตรงที่ข้อมูลภายในแอพจะอัพเดตได้ตลอดเวลา อย่าง<br />

แอพประเภทเครือข่ายสังคม (Social Network) หรือการประมวลผลแบบคลาวด์ (Cloud) ก็มีการใช้<br />

คุณสมบัตินี้เช่นกัน<br />

ในบทนี้เราจะเน้นเรื่องของการใช้งานบริการข้อความสั้น (SMS), การดึงข้อมูลจากอินเตอร์เน็ต<br />

และการใช้งานแอพประเภทเครือข่ายสังคม โดยข้อความแบบ SMS นั้นจะเป็นบริการการแลกเปลี่ยน<br />

ข้อความระหว่างโทรศัพท์มือถือ ส่วนการดึงข้อมูลจากอินเตอร์เน็ตนั้น เราจะทำงานกับข้อมูลประเภท<br />

XML (Extensible Markup Language), HTML (HyperText Markup Language) และ JSON<br />

(JavaScript Object Notation) ขณะที่การใช้งานแอพประเภทเครือข่ายสังคมนั้น เราจะเชื่อมต่อกับ<br />

ทวิตเตอร์ (Twitter)<br />

การใช้งาน SMS<br />

ในเฟรมเวิร์คของระบบปฏิบัติการแอนดรอยด์จะมีไลบรารีที่เกี่ยวข้องกับการใช้บริการ SMS อยู่<br />

นั่นคือคลาส SmsManager ในแอนดรอยด์เวอร์ชั่นแรกๆ นั้น คำสั่ง SmsManager จะอยู่ในแพ็คเกจ<br />

android.telephony.gsm แต่หลังจากแอนดรอยด์เวอร์ชั่น 1.5 เป็นต้นมา SmsManager จะรองรับ<br />

การทำงานทั้ง GSM และ CDMA ซึ่งคำสั่ง SmsManager จะอยู่ในแพ็คเกจ android.telephony<br />

แทน<br />

การส่งข้อความ SMS ด้วยคลาส SmsManager นั้น มีขั้นตอนดังนี้<br />

1. กำหนดสิทธิ์การใช้งานในไฟล์ Manifest ตามนี้<br />

<br />

2. ใช้คำสั่ง SmsManager.getDefault() เพื่อสร้างอินสแตนซ์ของคลาส<br />

SmsManager mySMS = SmsManager.getDefault();<br />

3. กำหนดเลขหมายปลายทางที่จะส่งข้อความ SMS รวมถึงข้อความที่จะส่งด้วยคำสั่ง<br />

sendTextMessage()<br />

sendTextMesssage() method to send the SMS to another device:<br />

String destination = “16501234567”;<br />

String msg = “Sending my first message”;<br />

mySMS.sendTextMessage(destination, null, msg, null, null);


196 บทที่ 8 เครือข่าย<br />

ขั้นตอนข้างต้นนี้เป็นลำดับของการส่งข้อความ SMS แต่อย่างไรก็ตาม เราจะต้องกำหนดค่า<br />

เพิ่มเติมอีก 3 อย่าง เพื่อให้การทำงานที่สมบูรณ์ ดังนี้<br />

m ค่าของศูนย์บริการข้อความ SMS ซึ่งถ้ากำหนดค่านี้เป็น null ระบบจะใช้ค่าที่กำหนดมา<br />

จากผู้ให้บริการเครือข่ายโทรศัพท์<br />

m ค่าของ PendingIntent เพื่อตรวจสอบสถานะการส่งข้อความ<br />

m ค่าของ PendingIntent เพื่อตรวจสอบสถานะการรับข้อความ<br />

การใช้งานค่า PendingIntent เพื่อตรวจสอบสถานะของข้อความที่รับ-ส่งนั้น ใช้คำสั่งดังนี้<br />

String SENT_SMS_FLAG = “SENT_SMS”;<br />

String DELIVER_SMS_FLAG = “DELIVER_SMS”;<br />

Intent sentIn = new Intent(SENT_SMS_FLAG);<br />

PendingIntent sentPIn = PendingIntent.getBroadcast(this,0,sentIn,0);<br />

Intent deliverIn = new Intent(SENT_SMS_FLAG);<br />

PendingIntent deliverPIn<br />

= PendingIntent.getBroadcast(this,0,deliverIn,0);<br />

จากนั้น BroadcastReceiver จำเป็นต้องถูกรีจีสเตอร์ให้กับ PendingIntent แต่ละตัวเพื่อ<br />

ให้ได้ผลลัพธ์ดังนี้<br />

BroadcastReceiver sentReceiver = new BroadcastReceiver(){<br />

@Override public void onReceive(Context c, Intent in) {<br />

switch(getResultCode()){<br />

case Activity.RESULT_OK:<br />

//sent SMS message successfully;<br />

break;<br />

default:<br />

//sent SMS message failed<br />

break;<br />

}<br />

}<br />

};<br />

BroadcastReceiver deliverReceiver = new BroadcastReceiver(){<br />

@Override public void onReceive(Context c, Intent in) {<br />

//SMS delivered actions<br />

}<br />

};<br />

registerReceiver(sentReceiver, new IntentFilter(SENT_SMS_FLAG));<br />

registerReceiver(deliverReceiver, new IntentFilter(DELIVER_SMS_FLAG));<br />

ขนาดของข้อความ SMS ส่วนใหญ่จะจำกัดอยู่ที่ 140 ตัวอักษรต่อข้อความ โดยกระบวนการ<br />

ตรวจสอบเพื่อให้แน่ใจว่าข้อความที่จะส่งนั้นมีความยาวไม่เกินจากที่กำหนดไว้ เราจะใช้เมธอด<br />

divideMessage() เพื่อตัดข้อความในกรณีที่ข้อความมีขนาดยาวเกินกว่า 140 ตัวอักษร


การใช้งาน SMS<br />

197<br />

หลังจากการใช้คำสั่ง divideMessage() เพื่อตัดข้อความแล้ว เราจะใช้เมธอด sendMulti-<br />

PartMessage() ทำการส่งข้อความชุดดังกล่าว ซึ่งวิธีการใช้งานคำสั่งนี้จะเหมือนกับการใช้คำสั่ง<br />

sendMultipartTextMessage() โดยจะใช้ ArrayList เพื่อเก็บข้อมูลของข้อความที่ส่ง และ<br />

สถานะการรับ-ส่ง ดังนี้<br />

ArrayList multiSMS = mySMS.divideMessage(msg);<br />

ArrayList sentIns = new ArrayList();<br />

ArrayList deliverIns = new ArrayList();<br />

for(int i=0; i< multiSMS.size(); i++){<br />

sentIns.add(sentIn);<br />

deliverIns.add(deliverIn);<br />

}<br />

mySMS.sendMultipartTextMessage(destination, null,<br />

multiSMS, sentIns, deliverIns);<br />

กรรมวิธี: การส่ง SMS แบบอัตโนมัติเมื่อมีการรับข้อความแล้ว<br />

ในกรณีที่ผู้รับได้รับข้อความ SMS แล้ว แต่ยังไม่ได้อ่านข้อความดังกล่าว เราก็สามารถเขียนชุด<br />

คำสั่งเพื่อสั่งให้ระบบส่ง SMS แบบอัตโนมัติเมื่อมีการรับข้อความแล้วได้ ในกรณีนี้เราจะสร้างเซอร์วิส<br />

แบบทำงานแบบเบื้องหลังเพื่อคอยรับข้อความ SMS ที่ส่งเข้ามา<br />

การทำงานของกระบวนการนี้จะต้องมีการกำหนดสิทธิ์ในการใช้งาน โดยกำหนดไว้ในไฟล์<br />

Manifest ดังแสดงในชุดคำสั่งที่ 8.1 ซึ่งมีการประกาศแอคทิวิตี้ SMSResponder ที่จะใช้สร้างระบบ<br />

ตอบกลับแบบอัตโนมัติ และเซอร์วิส ResponderService ซึ่งจะใช้ในการส่งข้อความตอบกลับ<br />

ชุดคำสั่งที่ 8.1 AndroidManifest.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />


198 บทที่ 8 เครือข่าย<br />

ชุดคำสั่งที่ 8.2 จะแสดงเลย์เอาต์หลักที่ใช้ในการทำงานนี้ที่มีการสร้าง LinearLayout จำนวน<br />

3 วิว คือ TextView จะใช้ในการแสดงข้อความที่จะตอบกลับอัตโนมัติ, ปุ่มที่ใช้ยืนยันการเปลี่ยนแปลง<br />

ที่เกิดขึ้นกับข้อความตอบกลับ และ EditText ที่ให้ผู้ใช้ได้กรอกข้อความที่จะตอบกลับ<br />

ชุดคำสั่งที่ 8.2 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

แอคทิวิตี้หลักที่แสดงในชุดคำสั่งที่ 8.3 จะเริ่มการทำงานของเซอร์วิสที่ใช้ในการตอบกลับ<br />

อัตโนมัติ ซึ่งผู้ใช้สามารถเปลี่ยนข้อความที่จะตอบกลับ และจัดเก็บไว้ใน SharedPreference เพื่อใช้<br />

อีกครั้งในภายหลังได้<br />

ชุดคำสั่งที่ 8.3 src/com/cookbook/SMSresponder/SMSResponder.java<br />

package com.cookbook.SMSresponder;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.content.SharedPreferences;<br />

import android.content.SharedPreferences.Editor;<br />

import android.os.Bundle;


import android.preference.PreferenceManager;<br />

import android.util.Log;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

import android.widget.EditText;<br />

import android.widget.TextView;<br />

การใช้งาน SMS<br />

199<br />

public class SMSResponder extends Activity {<br />

TextView tv1;<br />

EditText ed1;<br />

Button bt1;<br />

SharedPreferences myprefs;<br />

Editor updater;<br />

String reply=null;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

myprefs = PreferenceManager.getDefaultSharedPreferences(this);<br />

tv1 = (TextView) this.findViewById(R.id.display);<br />

ed1 = (EditText) this.findViewById(R.id.editText);<br />

bt1 = (Button) this.findViewById(R.id.submit);<br />

reply = myprefs.getString("reply",<br />

"Thank you for your message. I am busy now. "<br />

+ "I will call you later");<br />

tv1.setText(reply);<br />

updater = myprefs.edit();<br />

ed1.setHint(reply);<br />

bt1.setOnClickListener(new OnClickListener() {<br />

public void onClick(View view) {<br />

updater.putString("reply", ed1.getText().toString());<br />

updater.commit();<br />

SMSResponder.this.finish();<br />

}<br />

});<br />

try {<br />

// start Service<br />

Intent svc = new Intent(this, ResponderService.class);<br />

startService(svc);<br />

}


200 บทที่ 8 เครือข่าย<br />

}<br />

}<br />

catch (Exception e) {<br />

Log.e("onCreate", "service creation problem", e);<br />

}<br />

การทำงานหลักของงานชิ้นนี้จะกำหนดไว้ในเซอร์วิสดังแสดงไว้ในชุดคำสั่งที่ 8.4 ซึ่งจะเรียกใช้<br />

ค่าของ SharedPreferences เป็นอันดับแรก และประกาศ Broadcast Receiver เพื่อตรวจจับ<br />

ข้อความ SMS ที่ส่งเข้า-ออก ซึ่งในการตรวจจับข้อความที่ส่งออกนั้นจะไม่ได้กล่าวถึงในหัวข้อนี้ แต่ใน<br />

ชุดคำสั่งจะมีใส่มาเพื่อให้โครงสร้างของคำสั่งมีความสมบูรณ์<br />

ในการตรวจจับข้อความ SMS ที่ส่งเข้ามานั้นจะใช้ค่าของ PDU (Protocol Description Unit)<br />

ซึ่งประกอบด้วย ข้อความ SMS ค่าต่างๆ ที่ใช้ในการส่งโดยเก็บไว้ในอาร์เรย์ Object และเมธอด<br />

createFromPdu() ใช้แปลงข้อมูลอาร์เรย์ Object ไปเป็น SmsMessage ขั้นตอนต่อไปให้ใช้เมธอด<br />

getOriginatingAddress() เพื่ออ่านค่าเบอร์โทรศัพท์ของผู้ส่ง และใช้คำสั่ง getMessageBody()<br />

เพื่ออ่านค่าข้อความ SMS<br />

หลังจากที่อ่านค่าเบอร์โทรศัพท์ของผู้ส่งแล้ว เราจะเรียกใช้เมธอด respond() ซึ่งเมธอดนี้จะ<br />

อ่านข้อมูลจาก SharedPreferences เพื่อใช้ในการตอบกลับอัตโนมัติ ถ้าไม่พบข้อมูลใดๆ ระบบก็จะ<br />

ใช้ค่าดีฟอลต์ที่เก็บไว้ในระบบอยู่แล้วแทน ในขั้นตอนต่อไปจะเป็นการสร้างอินเท็นต์จำนวน 2 ตัว คือ<br />

PendingIntents เพื่อรายงานผลการรับ-ส่ง และ devideMessage เพื่อตรวจสอบข้อความว่าไม่มี<br />

ขนาดใหญ่เกินกว่าที่กำหนด หลังจากที่เตรียมข้อมูลต่างๆ พร้อมแล้ว เราก็จะใช้คำสั่ง sendMuilt-<br />

TextMessage() เพื่อส่งข้อความดังกล่าว<br />

ชุดคำสั่งที่ 8.4 src/com/cookbook/SMSresponder/ResponderService.java<br />

package com.cookbook.SMSresponder;<br />

import java.util.ArrayList;<br />

import android.app.Activity;<br />

import android.app.PendingIntent;<br />

import android.app.Service;<br />

import android.content.BroadcastReceiver;<br />

import android.content.Context;<br />

import android.content.Intent;<br />

import android.content.IntentFilter;<br />

import android.content.SharedPreferences;<br />

import android.os.Bundle;<br />

import android.os.IBinder;<br />

import android.preference.PreferenceManager;<br />

import android.telephony.SmsManager;<br />

import android.telephony.SmsMessage;<br />

import android.util.Log;<br />

import android.widget.Toast;


public class ResponderService extends Service {<br />

การใช้งาน SMS<br />

201<br />

//The Action fired by the Android-System when a SMS was received.<br />

private static final String RECEIVED_ACTION =<br />

"android.provider.Telephony.SMS_RECEIVED";<br />

private static final String SENT_ACTION="SENT_SMS";<br />

private static final String DELIVERED_ACTION="DELIVERED_SMS";<br />

String requester;<br />

String reply="";<br />

SharedPreferences myprefs;<br />

@Override<br />

public void onCreate() {<br />

super.onCreate();<br />

myprefs = PreferenceManager.getDefaultSharedPreferences(this);<br />

registerReceiver(sentReceiver, new IntentFilter(SENT_ACTION));<br />

registerReceiver(deliverReceiver,<br />

new IntentFilter(DELIVERED_ACTION));<br />

IntentFilter filter = new IntentFilter(RECEIVED_ACTION);<br />

registerReceiver(receiver, filter);<br />

}<br />

IntentFilter attemptedfilter = new IntentFilter(SENT_ACTION);<br />

registerReceiver(sender,attemptedfilter);<br />

private BroadcastReceiver sender = new BroadcastReceiver(){<br />

@Override<br />

public void onReceive(Context c, Intent i) {<br />

if(i.getAction().equals(SENT_ACTION)) {<br />

if(getResultCode() != Activity.RESULT_OK) {<br />

String reciptent = i.getStringExtra("recipient");<br />

requestReceived(reciptent);<br />

}<br />

}<br />

}<br />

};<br />

BroadcastReceiver sentReceiver = new BroadcastReceiver() {<br />

@Override public void onReceive(Context c, Intent in) {<br />

switch(getResultCode()) {<br />

case Activity.RESULT_OK:<br />

//sent SMS message successfully;<br />

smsSent();<br />

break;<br />

default:<br />

//sent SMS message failed<br />

smsFailed();<br />

break;


202 บทที่ 8 เครือข่าย<br />

};<br />

}<br />

}<br />

public void smsSent() {<br />

Toast.makeText(this, "SMS sent", Toast.LENGTH_SHORT);<br />

}<br />

public void smsFailed() {<br />

Toast.makeText(this, "SMS sent failed", Toast.LENGTH_SHORT);<br />

}<br />

public void smsDelivered() {<br />

Toast.makeText(this, "SMS delivered", Toast.LENGTH_SHORT);<br />

}<br />

BroadcastReceiver deliverReceiver = new BroadcastReceiver() {<br />

@Override public void onReceive(Context c, Intent in) {<br />

//SMS delivered actions<br />

smsDelivered();<br />

}<br />

};<br />

public void requestReceived(String f) {<br />

Log.v("ResponderService","In requestReceived");<br />

requester=f;<br />

}<br />

BroadcastReceiver receiver = new BroadcastReceiver() {<br />

@Override<br />

public void onReceive(Context c, Intent in) {<br />

Log.v("ResponderService","On Receive");<br />

reply="";<br />

if(in.getAction().equals(RECEIVED_ACTION)) {<br />

Log.v("ResponderService","On SMS RECEIVE");<br />

}<br />

}<br />

Bundle bundle = in.getExtras();<br />

if(bundle!=null) {<br />

Object[] pdus = (Object[])bundle.get("pdus");<br />

SmsMessage[] messages = new SmsMessage[pdus.length];<br />

for(int i = 0; i


};<br />

การใช้งาน SMS<br />

203<br />

@Override<br />

public void onStart(Intent intent, int startId) {<br />

super.onStart(intent, startId);<br />

}<br />

public void respond() {<br />

Log.v("ResponderService","Responing to " + requester);<br />

reply = myprefs.getString("reply",<br />

"Thank you for your message. I am busy now. "<br />

+ "I will call you later");<br />

SmsManager sms = SmsManager.getDefault();<br />

Intent sentIn = new Intent(SENT_ACTION);<br />

PendingIntent sentPIn = PendingIntent.getBroadcast(this,<br />

0,sentIn,0);<br />

Intent deliverIn = new Intent(DELIVERED_ACTION);<br />

PendingIntent deliverPIn = PendingIntent.getBroadcast(this,<br />

0,deliverIn,0);<br />

ArrayList Msgs = sms.divideMessage(reply);<br />

ArrayList sentIns = new ArrayList();<br />

ArrayList deliverIns =<br />

new ArrayList();<br />

for(int i=0; i< Msgs.size(); i++) {<br />

sentIns.add(sentPIn);<br />

deliverIns.add(deliverPIn);<br />

}<br />

}<br />

sms.sendMultipartTextMessage(requester, null,<br />

Msgs, sentIns, deliverIns);<br />

@Override<br />

public void onDestroy() {<br />

super.onDestroy();<br />

unregisterReceiver(receiver);<br />

unregisterReceiver(sender);<br />

}<br />

}<br />

@Override<br />

public IBinder onBind(Intent arg0) {<br />

return null;<br />

}


204 บทที่ 8 เครือข่าย<br />

การทำงานกับข้อมูลบนเว็บ<br />

การสั่งให้ระบบเปิดใช้อินเตอร์เน็ตบราวเซอร์เพื่อแสดงข้อมูลเว็บนั้น เราจะต้องใช้งานอินเท็นต์<br />

ACTION_VIEW ตามตัวอย่างดังนี้<br />

Intent i = new Intent(Intent.ACTION_VIEW);<br />

i.setData(Uri.parse(“http://www.google.com”));<br />

startActivity(i);<br />

เราสามารถพัฒนาบราวเซอร์ขึ้นเองได้ด้วยการใช้ไลบรารี WebView ซึ่งเป็นวิวที่ใช้แสดงข้อมูล<br />

จากเว็บ และเช่นเดียวกับการทำงานของวิวอื่นๆ WebView สามารถแสดงผลแบบเต็มจอหรือแสดงไว้<br />

ในบางส่วนของวิวได้ การทำงานของ WebView นั้นจะใช้ชุดคำสั่งของ WebKit ซึ่งเป็นไลบรารีแบบ<br />

โอเพ่นซอร์สที่ใช้แสดงข้อมูลเว็บเพจ โดยบราวเซอร์ Safari ของ Apple ก็นำไปใช้งานเช่นกัน<br />

กรรมวิธี: การปรับแต่งเวบบราวเซอร์<br />

ในการใช้งานออบเจ็กต์ WebView นั้นมีอยู่ 2 วิธี คือ การสร้างการคลาสโดยตรง<br />

WebView webview = new WebView(this);<br />

หรือการประกาศไว้ในแอคทิวิตี้และนำมาใช้ภายในเลย์เอาต์<br />

WebView webView = (WebView) findViewById(R.id.webview);<br />

หลังจากที่สร้างอินสแตนซ์ของ WebView แล้ว เราก็จะใช้คำสั่ง loadURL() เพื่อให้แสดงหน้า<br />

ของเว็บเพจที่ต้องการ ดังนี้<br />

webview.loadUrl(“http://www.google.com/”);<br />

คลาส WebSettings จะใช้ในการประกาศคุณสมบัติการทำงานของบราวเซอร์ อย่างเช่นว่าเรา<br />

สามารถปิดกั้นการแสดงผลรูปภาพในบราวเซอร์เพื่อลดปริมาณการใช้ข้อมูล และเพิ่มความเร็วในการ<br />

ประมวลผลได้ด้วยการใช้คำสั่ง BlockNetworkImage() หรือการกำหนดขนาดของฟอนต์ที่จะแสดง<br />

ในบราวเซอร์โดยใช้คำสั่ง setDefaultFontSize() ซึ่งเราสามารถกำหนดค่าอื่นๆ ได้อีก ดังนี้<br />

WebSettings webSettings = webView.getSettings();<br />

webSettings.setSaveFormData(false);<br />

webSettings.setJavaScriptEnabled(true);<br />

webSettings.setSavePassword(false);<br />

webSettings.setSaveFormData(false);<br />

webSettings.setJavaScriptEnabled(true);<br />

webSettings.setSupportZoom(true);<br />

กรรมวิธี: การใช้งาน HTTP GET<br />

ในขั้นตอนของการเรียกใช้งานบราวเซอร์โดยใช้ WebView นั้น ชุดคำสั่ง WebKit จะถูกใช้ไปกับ<br />

การทำงานดังกล่าว ซึ่งเราสามารถสร้างแอพที่ทำงานร่วมกับอินเตอร์เน็ตได้ โดยหมายความว่าข้อมูลที่<br />

แอพใช้ทำงานนั้นจะถูกดึงมาจากอินเตอร์เน็ต เช่น รูปภาพต่าง, ไฟล์มัลติมีเดีย รวมทั้งข้อมูลแบบ<br />

XML โดยลักษณะการทำงานนี้มักพบในแอพประเภทเครือข่ายสังคม แพ็คเกจของแอนดรอยด์ที่ใช้ใน<br />

การจัดการการสื่อสารข้อมูลในเครือข่าย คือ java.net และ android.net


ในหัวข้อนี้เราจะใช้คำสั่ง HTTP GET ในการอ่านข้อมูล XML หรือ JSON ซึ่งขั้นตอนและวิธี<br />

การใช้งานข้อมูลเหล่านี้ รวมทั้ง API ที่เกี่ยวข้อง ถูกแสดงไว้ใน URL ของ Google ดังนี้<br />

http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=<br />

205<br />

ในการค้นหาหัวข้ออื่นๆ นั้น เราจะต้องเพิ่มหัวข้อลงไปในคำสั่งคิวรี่ข้อมูล ยกตัวอย่างเช่น การที่<br />

จะค้นหาข้อมูลเกี่ยวกับ National Basketball Association (NBA) ก็จะใช้คิวรีเพื่อแสดงข้อมูลแบบ<br />

JSON ดังนี้<br />

http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=NBA<br />

แอคทิวิตี้นี้จำเป็นจะต้องใช้สิทธิ์ในการเชื่อมต่ออินเตอร์เน็ต ดังนั้นเราจะต้องกำหนดสิทธิ์<br />

ดังกล่าวไว้ในไฟล์ Manifest ด้วย ดังนี้<br />

<br />

ชุดคำสั่งที่ 8.5 ได้แสดงเลย์เอาต์ของวิวจำนวน 3 วิว คือ EditText เพื่อให้ผู้ใช้กรอกข้อมูลที่<br />

ต้องการจะค้นหา, Button เพื่อสั่งให้เริ่มการค้นหา และ TextView เพื่อใช้ในการแสดงผลลัพธ์ของ<br />

การค้นหา<br />

ชุดคำสั่งที่ 8.5 res/layout/main.xml<br />

การทำางานกับข้อมูลบนเว็บ<br />

<br />

<br />

<br />

<br />

<br />


206 บทที่ 8 เครือข่าย<br />

แอคทิวิตี้หลักที่แสดงในชุดคำสั่งที่ 8.6 เป็นการเริ่มการทำงานของวิวด้วยคำสั่ง onCreate()<br />

และตรวจจับการทำงานของปุ่มด้วย onClickListener และเมื่อมีการกดปุ่มมันก็จะเริ่มทำงานด้วย<br />

คำสั่ง SearchRequest() ซึ่งการค้นหานี้เราจะส่งค่าไปยัง Google ด้วยคำสั่งค้นหาข้อมูล<br />

ที่มีลักษณะเป็น URL และจะใช้คลาส URL ในการสร้างอินสแตนซ์ HttpURLConnection เพื่อส่งค่า<br />

URL<br />

อินสแตนซ์ HttpURLConnection สามารถอ่านสถานะของการเชื่อมต่อได้ด้วย เมื่อ HttpURL-<br />

Connection ส่งค่าของตัวแปร HTTP กลับมาถูกต้อง นั่นหมายความว่าการเชื่อมต่อไม่มีข้อผิดพลาด<br />

ข้อมูลแบบ JSON ที่ระบบตอบกลับมานั้น เราจะใช้คำสั่ง InputStreamReader ในการส่งค่าไปยัง<br />

เมธอด BufferReader เพื่ออ่านข้อมูลและสร้างอินสแตนซ์ของข้อความ และหลังจากที่ได้รับผลการ<br />

ค้นหาแล้ว เราจะใช้ฟังก์ชั่น ProcessResponse() ในการแปลข้อมูลแบบ JSON ซึ่ง API ของ Web-<br />

Kit ได้แสดงข้อมูลที่ค้นหาได้เอาไว้ใน JSONArray ดังรูปที่ 8.1 แสดงผลลัพธ์จากการค้นหาข้อความ<br />

NBA<br />

ชุดคำสั่งที่ 8.6 src/com/cookbook/internet/search/GoogleSearch.java<br />

package com.cookbook.internet.search;<br />

import java.io.BufferedReader;<br />

import java.io.IOException;<br />

import java.io.InputStreamReader;<br />

import java.net.HttpURLConnection;<br />

import java.net.MalformedURLException;<br />

import java.net.URL;<br />

import java.security.NoSuchAlgorithmException;<br />

import org.json.JSONArray;<br />

import org.json.JSONException;<br />

import org.json.JSONObject;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.util.Log;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

import android.widget.EditText;<br />

import android.widget.TextView;<br />

public class GoogleSearch extends Activity {<br />

/** Called when the activity is first created. */<br />

TextView tv1;<br />

EditText ed1;<br />

Button bt1;<br />

static String url =<br />

"http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=";


การทำางานกับข้อมูลบนเว็บ<br />

207<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv1 = (TextView) this.findViewById(R.id.display);<br />

ed1 = (EditText) this.findViewById(R.id.editText);<br />

bt1 = (Button) this.findViewById(R.id.submit);<br />

}<br />

bt1.setOnClickListener(new OnClickListener() {<br />

public void onClick(View view) {<br />

if(ed1.getText().toString()!=null) {<br />

try{<br />

ProcessResponse(<br />

SearchRequest(ed1.getText().toString()));<br />

} catch(Exception e) {<br />

Log.v("Exception google search",<br />

"Exception:"+e.getMessage());<br />

}<br />

}<br />

ed1.setText("");<br />

}<br />

});<br />

public String SearchRequest(String searchString)<br />

throws MalformedURLException, IOException {<br />

String newFeed=url+searchString;<br />

StringBuilder response = new StringBuilder();<br />

Log.v("gsearch","gsearch url:"+newFeed);<br />

URL url = new URL(newFeed);<br />

HttpURLConnection httpconn<br />

= (HttpURLConnection) url.openConnection();<br />

}<br />

if(httpconn.getResponseCode()==HttpURLConnection.HTTP_OK) {<br />

BufferedReader input = new BufferedReader(<br />

new InputStreamReader(httpconn.getInputStream()),<br />

8192);<br />

String strLine = null;<br />

while ((strLine = input.readLine()) != null) {<br />

response.append(strLine);<br />

}<br />

input.close();<br />

}<br />

return response.toString();


208 บทที่ 8 เครือข่าย<br />

}<br />

public void ProcessResponse(String resp) throws IllegalStateException,<br />

IOException, JSONException, NoSuchAlgorithmException {<br />

StringBuilder sb = new StringBuilder();<br />

Log.v("gsearch","gsearch result:"+resp);<br />

JSONObject mResponseObject = new JSONObject(resp);<br />

JSONObject responObject<br />

= mResponseObject.getJSONObject("responseData");<br />

JSONArray array = responObject.getJSONArray("results");<br />

Log.v("gsearch","number of resultst:"+array.length());<br />

for(int i = 0; i


กรรมวิธี: การใช้งาน HTTP POST<br />

ในบางครั้งเราอาจต้องการอ่านข้อมูลประเภทไบนารี (Binary) จาากอินเตอร์เน็ต เช่น รูปภาพ<br />

วิดีโอ รวมถึงไฟล์เสียงต่างๆ ถ้าเป็นแบบนั้นก็ให้ใช้คำสั่ง setRequestMethod() เพื่อใช้งาน<br />

โปรโตคอล HTTP POST โดยใช้ setRequestMethod() เช่น<br />

httpconn.setRequestMethod(POST);<br />

การทำางานกับข้อมูลบนเว็บ<br />

การอ่านข้อมูลจากอินเตอร์เน็ตอาจใช้ระยะเวลานาน คาดเดาไม่ได้ ดังนั้นเราจะดึงเธรดย่อยมา<br />

ช่วยทำงานในส่วนนี้เพื่อไม่ให้เกิดอาการแอพค้างในระหว่างที่กำลังอ่านข้อมูลอยู่<br />

การสร้างเธรดเบื้องหลังเพื่อทำงานเหล่านี้นั้น ในระบบปฏิบัติการแอนดรอยด์มีคำสั่ง Async-<br />

Task เพื่อใช้ทำงานแบบเบื้องหลัง และส่งผลลัพธ์ไปยังเธรดของ UI โดยไม่จำเป็นต้องสร้างเธรดขึ้น<br />

มาเอง ดังนั้นเราจึงสามารถนำเมธอด POST ไปใช้งานร่วมกับแอคทิวิตี้หลักได้ ดังนี้<br />

private class mygoogleSearch extends AsyncTask {<br />

209<br />

protected String doInBackground(String... searchKey) {<br />

String key = searchKey[0];<br />

}<br />

try {<br />

return SearchRequest(key);<br />

} catch(Exception e) {<br />

Log.v(“Exception google search”,<br />

“Exception:”+e.getMessage());<br />

return “”;<br />

}<br />

}<br />

protected void onPostExecute(String result) {<br />

try {<br />

ProcessResponse(result);<br />

} catch(Exception e) {<br />

Log.v(“Exception google search”,<br />

“Exception:”+e.getMessage());<br />

}<br />

}<br />

คำสั่งนี้สามารถนำไปเพิ่มต่อท้ายคำสั่งในไฟล์ GoogleSearch.java ในชุดคำสั่งที่ 8.6 ได้<br />

ซึ่งผลลัพธ์ที่ได้ก็จะเหมือนกับในตัวอย่างก่อนหน้านี้ และมีการเปลี่ยนแปลงในส่วนท้ายของ onClick-<br />

Listener ดังนี้<br />

new mygoogleSearch().execute(ed1.getText().toString());


210 บทที่ 8 เครือข่าย<br />

เครือข่ายสังคม<br />

ทวิตเตอร์ (Twitter) เป็นเว็บแอพประเภทเครือข่ายสังคม มีลักษณะเป็นไมโครบล็อกกิ้ง<br />

ที่อนุญาตให้ผู้ใช้สามารถรับส่งข้อความที่เรียกว่า “ทวีต” (Tweet) ได้ มีคนให้คำนิยามว่าทวิตเตอร์เป็น<br />

SMS บนอินเตอร์เน็ต ข้อความในทวิตเตอร์จะพิมพ์ได้มากสุดที่ 140 ตัวอักษร และผู้ที่ใช้ทวิตเตอร์<br />

สามารถติดตามการส่งข้อความทวิตเตอร์ของผู้ใช้คนอื่นได้<br />

กรรมวิธี: การเชื่อมต่อกับทวิตเตอร์<br />

มีผู้ผลิตหลายรายพัฒนาไลบรารีที่ใช้ในการเชื่อมต่อทวิตเตอร์กับแอพแอนดรอยด์ http://dev.<br />

twitter.com/pages/libraries#java) ดังนี้<br />

m Twitter4J พัฒนาโดย ยูสุเกะ ยามาโมโต – เป็นไลบรารีประเภทจาวาโอเพนซอร์ส<br />

ใช้ลิขสิทธิ์ของ BSD<br />

m java-twitter พัฒนาโดย เดวิด คลินตัน – เป็นจาวาอินเตอร์เฟซเพื่อใช้งานร่วมกับ<br />

ทวิตเตอร์<br />

m jtwitter พัฒนาโดย แดเนียล วินเทอร์สเตน – เป็นไลบรารีประเภทจาวาโอเพนซอร์ส<br />

m Twitter Client พัฒนาโดยบริษัท Gist,Inc – เป็นแอพลูกข่ายที่ใช้ในการติดต่อกับข้อมูล<br />

แบบสตรีมมิ่ง<br />

ในหัวข้อนี้เราจะใช้ Twitter4Jj ในการเขียนแอพ โดยคุณสามารถหาอ่านเพิ่มเติมได้ที่ http://<br />

twitter4j.org/en/javadoc/overview-summary.html การทำงานของชุดคำสั่งในหัวข้อนี้จะให้ผู้ใช้<br />

ล็อกอินเข้าสู่ทวิตเตอร์และส่งข้อความ หลังจากที่ส่งข้อความแล้วแอพก็จะอัพเดตข้อความที่ส่ง และ<br />

แสดงให้เห็นบนจอภาพ<br />

เราจะต้องกำหนดเลย์เอาต์แสดงผลจำนวน 2 เลย์เอาต์ คือ ส่วนที่ใช้ล็อกอินเข้าสู่ระบบ และ<br />

ส่วนที่ใช้แสดงข้อความทวิตเตอร์ ดังแสดงในรูปที่ 8.2 ขณะที่ส่วนของการกำหนดสิทธิ์ในการใช้งานนั้น<br />

จะกำหนดไว้ในไฟล์ Manifest ดังแสดงในชุดคำสั่งที่ 8.7<br />

รูปที่ 8.2 รูปซ้ายเป็นหน้าล็อกอิน และรูปขวาเป็นหน้าที่แสดงการอัพเดตของทวิตเตอร์


ชุดคำสั่งที่ 8.7 AndroidManifest.xml<br />

เครือข่ายสังคม<br />

211<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

ไฟล์เลย์เอาต์ที่จะใช้ในกรณีนี้ ก็คือ<br />

m login.xml – หน้าล็อกอินเข้าสู่ระบบ<br />

m main.xml – หน้าที่แสดงการอัพเดตสถานะของทวิตเตอร์ และสถานะ Home ตามการ<br />

ทำงานของชุดคำสั่งที่ 8.9<br />

m usertimelinerow.xml – หน้าที่แสดงสถานะของไทม์ไลน์ตามการทำงานของชุดคำสั่งที่<br />

8.10<br />

ชุดคำสั่งที่ 8.8 res/layout/login.xml<br />

<br />

<br />

<br />


212 บทที่ 8 เครือข่าย<br />

<br />

<br />

<br />

<br />

ชุดคำสั่งที่ 8.9 res/layout/main.xml<br />

<br />

<br />

<br />

<br />


214 บทที่ 8 เครือข่าย<br />

ชุดคำสั่งที่ 8.11 src/com/cookbook/twitter/TwitterCookBook.java<br />

package com.cookbook.twitter;<br />

import twitter4j.Twitter;<br />

import twitter4j.TwitterFactory;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.content.SharedPreferences;<br />

import android.content.SharedPreferences.Editor;<br />

import android.os.Bundle;<br />

import android.preference.PreferenceManager;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

import android.widget.EditText;<br />

import android.widget.Toast;<br />

public class TwitterCookBook extends Activity {<br />

SharedPreferences myprefs;<br />

EditText userET, passwordET;<br />

Button loginBT;<br />

static Twitter twitter;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

myprefs = PreferenceManager.getDefaultSharedPreferences(this);<br />

final String username = myprefs.getString("username", null);<br />

final String password = myprefs.getString("password", null);<br />

setContentView(R.layout.login);<br />

userET = (EditText)findViewById(R.id.userText);<br />

passwordET = (EditText)findViewById(R.id.passwordText);<br />

loginBT = (Button)findViewById(R.id.loginButton);<br />

userET.setText(username);<br />

passwordET.setText(password);<br />

loginBT.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

try {<br />

twitter = new TwitterFactory()<br />

.getInstance(userET.getText().toString(),<br />

passwordET.getText().toString());<br />

twitter.getFollowersIDs();


เครือข่ายสังคม<br />

215<br />

Intent i = new Intent(TwitterCookBook.this,<br />

UpdateAndList.class);<br />

startActivity(i);<br />

Editor ed = myprefs.edit();<br />

ed.putString("username",userET.getText().toString());<br />

ed.putString("password",<br />

passwordET.getText().toString());<br />

ed.commit();<br />

finish();<br />

}<br />

}<br />

});<br />

}<br />

} catch (Exception e) {<br />

e.printStackTrace();<br />

Toast.makeText(TwitterCookBook.this, "login failed!!",<br />

Toast.LENGTH_SHORT).show();<br />

}<br />

ในชุดคำสั่งที่ 8.12 เมื่อล็อกอินสำเร็จแล้ว แอคทิวิตี้ UpdateAndList ก็จะเริ่มทำงาน โดยจะ<br />

แสดงออบเจ็กต์ EditText เพื่อให้ผู้ใช้สามารถกรอกข้อความได้รวมถึงปุ่มกดที่ใช้ในการยืนยันการส่ง<br />

ข้อความด้วย ซึ่งในออบเจ็กต์ Twitter จะมีตัวแปร ResponseList ที่ใช้เก็บข้อมูลที่ส่งมาจากออบเจ็กต์<br />

แอคทิวิตี้ getHomeTimeline() มีไว้เพื่ออ่านข้อมูลสถานะของไทม์ไลน์ที่แสดงอยู่ในหน้า<br />

Home ของทวิตเตอร์เมื่อล็อกอิน โดยคำสั่งที่เกี่ยวข้องกับการอ่านข้อมูลจากอินเตอร์เน็ตนั้นจะต้องใส่<br />

ไว้ใน AsyncTask เสมอเพื่อป้องกันหน้าจอค้างในกรณีที่ต้องใช้เวลาในการอ่านข้อมูลนาน เมธอด<br />

getHomeTimeline() จะถูกเรียกใช้ทุกครั้งเมื่อผู้ใช้ส่งข้อความใหม่หรืออัพเดตไทม์ไลน์<br />

คำสั่ง UserTimeLineAdapter ของคลาส BaseAdapter จะใช้ในการแสดงข้อมูลของสถานะ<br />

ไทม์ไลน์ ซึ่งคำสั่งนี้จะใช้ข้อมูล userTimeLine เพื่อแสดงข้อมูลใน ListView<br />

ใน ListActivity จะมีคลาสของ AsyncTask อยู่ 2 ตัว คือ setup และ loadstatus<br />

ซึ่งจะทำงานในลักษณะเดียวกันคือเรียกใช้คำสั่ง getHomeTimeline() แต่จะมีข้อแตกต่างตรงที่<br />

setup จะสร้าง ListActivity โดยใช้ UserTimeLineAdapter แต่ loadstatus จะส่งข้อมูลไป<br />

ยัง UserTimeLineAdapter โดยตรงเมื่อข้อมูลมีการเปลี่ยนแปลง<br />

ชุดคำสั่งที่ 8.12 src/com/cookbook/twitter/UpdateAndList.java<br />

package com.cookbook.twitter;<br />

import twitter4j.ResponseList;<br />

import twitter4j.Status;<br />

import twitter4j.Twitter;


216 บทที่ 8 เครือข่าย<br />

import android.app.ListActivity;<br />

import android.content.Context;<br />

import android.os.AsyncTask;<br />

import android.os.Bundle;<br />

import android.util.Log;<br />

import android.view.LayoutInflater;<br />

import android.view.View;<br />

import android.view.ViewGroup;<br />

import android.view.View.OnClickListener;<br />

import android.widget.BaseAdapter;<br />

import android.widget.Button;<br />

import android.widget.EditText;<br />

import android.widget.TextView;<br />

public class UpdateAndList extends ListActivity {<br />

EditText userET;<br />

Button updateBT;<br />

Twitter twitter;<br />

ResponseList userTimeline;<br />

UserTimeLineAdapter myAdapter;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

userET = (EditText)findViewById(R.id.userStatus);<br />

updateBT = (Button)findViewById(R.id.updateButton);<br />

twitter = TwitterCookBook.twitter;<br />

setup stup = new setup();<br />

stup.execute();<br />

updateBT.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

try {<br />

twitter.updateStatus(userET.getText().toString());<br />

loadstatus ldstatus = new loadstatus();<br />

ldstatus.execute();<br />

userET.setText("");<br />

} catch (Exception e) {<br />

e.printStackTrace();<br />

}<br />

}


เครือข่ายสังคม<br />

217<br />

}<br />

});<br />

private class UserTimeLineAdapter extends BaseAdapter{<br />

private LayoutInflater mInflater;<br />

public UserTimeLineAdapter(Context context) {<br />

mInflater = LayoutInflater.from(context);<br />

}<br />

@Override<br />

public int getCount() {<br />

return userTimeline.size();<br />

}<br />

@Override<br />

public Status getItem(int i) {<br />

return userTimeline.get(i);<br />

}<br />

@Override<br />

public long getItemId(int i) {<br />

return i;<br />

}<br />

@Override<br />

public View getView(int arg0, View arg1, ViewGroup arg2) {<br />

final ViewHolder holder;<br />

View v = arg1;<br />

if ((v == null) || (v.getTag() == null)) {<br />

v = mInflater.inflate(R.layout.usertimelinerow, null);<br />

holder = new ViewHolder();<br />

holder.mName = (TextView)v.findViewById(R.id.name);<br />

holder.mStatus = (TextView)v.findViewById(R.id.msg);<br />

v.setTag(holder);<br />

} else {<br />

holder = (ViewHolder) v.getTag();<br />

}<br />

holder.status= getItem(arg0);<br />

holder.mName.setText(holder.status.getUser().getName());<br />

holder.mStatus.setText(holder.status.getText());<br />

v.setTag(holder);


218 บทที่ 8 เครือข่าย<br />

}<br />

return v;<br />

}<br />

public class ViewHolder {<br />

Status status;<br />

TextView mName;<br />

TextView mStatus;<br />

}<br />

private class setup extends AsyncTask {<br />

protected String doInBackground(String... searchKey) {<br />

try{<br />

userTimeline = twitter.getHomeTimeline();<br />

return "";<br />

}catch(Exception e){<br />

Log.v("Exception Twitter query",<br />

"Exception:"+e.getMessage());<br />

return "";<br />

}<br />

}<br />

}<br />

protected void onPostExecute(String result) {<br />

try {<br />

myAdapter = new UserTimeLineAdapter(UpdateAndList.this);<br />

UpdateAndList.this.setListAdapter(myAdapter);<br />

} catch(Exception e) {<br />

Log.v("Exception Twitter query",<br />

"Exception:"+e.getMessage());<br />

}<br />

}<br />

private class loadstatus extends AsyncTask {<br />

protected String doInBackground(String... searchKey) {<br />

try {<br />

userTimeline = twitter.getHomeTimeline();<br />

return "";<br />

} catch(Exception e) {<br />

Log.v("Exception Twitter query",


เครือข่ายสังคม<br />

219<br />

}<br />

}<br />

"Exception:"+e.getMessage());<br />

return "";<br />

}<br />

}<br />

protected void onPostExecute(String result) {<br />

try {<br />

myAdapter.notifyDataSetChanged();<br />

} catch(Exception e) {<br />

Log.v("Exception twitter query",<br />

"Exception:"+e.getMessage());<br />

}<br />

}


220 บทที่ 9 การทำางานร่วมกับข้อมูล


221<br />

บทที่ 9<br />

การทำงานร่วมกับข้อมูล<br />

แอพของแอนดรอยด์ที่มีการทำงานที่ซับซ้อนจะมีการทำงานร่วมกับข้อมูลต่างๆ อยู่เสมอ ซึ่งการ<br />

จัดการข้อมูลเหล่านั้นจะขึ้นอยู่กับความต้องการและแหล่งจัดเก็บข้อมูล ในระบบปฏิบัติการแอนดรอยด์<br />

มีไลบรารีที่เกี่ยวข้องกับการทำงานนี้อยู่หลายตัว อย่างเช่น:<br />

m ข้อมูลประเภท Shared Prefernces เหมาะกับข้อมูลที่มีขนาดไม่ใหญ่มาก เช่น ค่าการ<br />

เริ่มต้นทำงานของแอพ เป็นต้น<br />

m ฐานข้อมูล SQLite เหมาะกับข้อมูลประเภทระเบียนที่มีรายการจำนวนมากๆ<br />

m ไฟล์ข้อมูลของจาวามีลักษณะเป็นข้อมูลประเภทสตรีม โดยใช้คำสั่ง InputFileStream<br />

และ OutputFileStream ในการอ่านและเขียนข้อมูล<br />

โดยปกติแล้วในระบบปฏิบัติการแอนดรอยด์จะมีคำสั่งที่ใช้ในการเก็บข้อมูลแบบพื้นฐานอยู่นั่น<br />

คือ เมธอด onSaveInstanceState() และ onRestoreInstanceState() ในบทนี้เราจะกล่าวถึง<br />

การใช้งานตัวจัดการข้อมูลตามรายการข้างต้น ซึ่งการเลือกใช้นั้นก็จะขึ้นอยู่กับประเภทการทำงานและ<br />

ปริมาณของข้อมูล<br />

Shared Preferences<br />

Shared Preferences เป็นรูปแบบการเก็บข้อมูลที่ใช้งานได้ง่าย มีลักษณะเป็นกลุ่มของค่า<br />

ตัวแปรต่างๆ ซึ่งจัดเก็บอยู่ในรูปไฟล์ XML ตัวอย่างเช่น ถ้าแอพ com.cookbook.datastorage<br />

ได้สร้าง Shared Preferences ขึ้นมาเพื่อเก็บข้อมูล ระบบก็จะสร้างไฟล์ XML เอาไว้ภายใต้ไดเร็กทอรี<br />

/data/data/com.cookbook.datastorage/shared_prefs โดยทั่วไปแล้วมักใช้ Shared<br />

Preferences เพื่อเก็บค่าเริ่มต้นการทำงานของแอพ รวมถึงค่าคอนฟิกต่างๆ และยังสามารถเก็บค่า<br />

ของ Username และ Password ที่ใช้ล็อกอินเข้าสู่ระบบเพื่อใช้ล็อกอินอัตโนมัติในภายหลังได้ด้วย<br />

ข้อมูลแบบ Shared Preferences สามารถเรียกใช้ได้โดยคอมโพเน็นต์ทุกตัวที่มีอยู่ในแอพนั้น


222 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

กรรมวิธี: การสร้างและอ่านข้อมูลจาก Shared Preferences<br />

ข้อมูลแบบ Shared Preferences สามารถเข้าถึงได้ด้วยการใช้เมธอด getPreferences()<br />

ซึ่งในตอนแรกระบบจะใช้ค่า Shared Preferences แบบดีฟอลต์ แต่ถ้าในระบบนั้นมีการใช้งาน<br />

Shared Preferences หลายไฟล์ เราก็สามารถระบุไฟล์ที่ต้องการใช้งานได้โดยใช้คำสั่ง<br />

getSharedPrefernces() โดยที่การเปิดไฟล์ Shared Preferences นั้นจะกำหนดสิทธิ์ในการใช้<br />

งานได้ ดังนี้<br />

m MODE_PRIVATE - เรียกใช้ได้เฉพาะแอพที่ทำงานอยู่<br />

m MODE_WORLD_READABLE - ทุกแอพสามารถอ่านไฟล์ XML นี้ได้<br />

m MODE_WORLD_WRITABLE - ทุกแอพสามารถเขียนไฟล์ XML นี้ได้<br />

หลังจากที่อ่านข้อมูลด้วยออบเจ็กต์ SharedPreferences แล้ว ออบเจ็กต์ Editor จะถูกเรียก<br />

ใช้เพื่อเขียนข้อมูลลงไปในไฟล์ XML ด้วยคำสั่ง put() ซึ่งชนิดของข้อมูลที่จะเขียนนั้นมีอยู่ 5 ชนิด คือ<br />

int, long, float, String และ boolean สำหรับคำสั่งด้านล่างนี้จะแสดงขั้นตอนการสร้างและ<br />

การเก็บข้อมูล Shared Preferences<br />

SharedPreferences prefs = getSharedPreferences(“myDataStorage”,<br />

MODE_PRIVATE);<br />

Editor mEditor = prefs.edit();<br />

mEditor.putString(“username”,”datastorageuser1”);<br />

mEditor.putString(“password”,”password1234”);<br />

mEditor.commit();<br />

The following shows how to retrieve shared preferences data:<br />

SharedPreferences prefs = getSharedPreferences(“myDataStorage”,<br />

MODE_PRIVATE);<br />

String username = prefs.getString(“username”, “”);<br />

String password = prefs.getString(“password”, “”);<br />

กรรมวิธี: การใช้งาน Preferences Framework<br />

ระบบปฏิบัติการแอนดรอยด์มีการสร้างเฟรมเวิร์คมาตรฐานที่ใช้ในการจัดเก็บข้อมูล Shared<br />

Preferences เพื่อให้สามารถใช้งานร่วมกับแอพอื่นๆ ได้ ซึ่งจะใช้วิธีการจัดกลุ่มของข้อมูลโดยแยกย่อย<br />

เป็นกลุ่มต่างๆ คำสั่ง PreferenceCategory จะใช้ในการประกาศกลุ่มของข้อมูล และคำสั่ง<br />

PreferenceScreen ใช้ในการแสดงกลุ่มของข้อมูล<br />

ในหัวข้อนี้จะใช้ข้อมูล Preferences ในไฟล์ XML ที่แสดงไว้ในชุดคำสั่งที่ 9.1 และใช้คำสั่ง<br />

PreferenceScreen เพื่อกำหนดรูทอีลีเมนต์ ซึ่งประกอบไปด้วยอีลีเมนต์ EditTextPreference<br />

จำนวน 2 ตัวเพื่อแทนข้อมูล Username และ Password รวมทั้งกำหนดอีลีเมนต์อื่นๆ อีกคือ<br />

CheckBoxPreference, RingTonePreference และ DialogPreference ระบบปฏิบัติการ<br />

แอนดรอยด์จะสร้าง UI ขึ้นมาเพื่อใช้ในการจัดการข้อมูล Preferences ดังแสดงไว้ในรูปที่ 9.1 ข้อมูล<br />

เหล่านี้จะเก็บอยู่ในรูปแบบ Shared Preference ซึ่งเราสามารถอ่านข้อมูลเหล่านี้โดยใช้คำสั่ง<br />

getPreferences()


ชุดคำสั่งที่ 9.1 res/xml/preferences.xml<br />

Shared Preferences<br />

223<br />

<br />

<br />

<br />

<br />

<br />

ขั้นตอนต่อไปแอคทิวิตี้จะสร้างอินสแตนซ์จากคลาส PreferenceActivity เพื่อใช้งานเมธอด<br />

addPreferencesFromResource() ในการรวมข้อมูล Shared Preference นี้เข้ากับแอคทิวิตี้<br />

ดังแสดงในชุดคำสั่งที่ 9.2 ดังนี้<br />

ชุดคำสั่งที่ 9.2 src/com/cookbook/datastorage/MyPreferences.java<br />

package com.cookbook.datastorage;<br />

import android.os.Bundle;<br />

import android.preference.PreferenceActivity;<br />

public class MyPreferences extends PreferenceActivity {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

}<br />

}<br />

addPreferencesFromResource(R.xml.preferences);<br />

แอคทิวิตี้หลักจะเรียกใช้ PreferenceActivity เมื่อต้องการใช้งาน (ในที่นี้หมายถึงการกดปุ่ม<br />

เมนู) ชุดคำสั่งที่ 9.3 จะแสดงให้เห็นถึงการแสดงข้อมูล Preferences ในระหว่างที่แอคทิวิตี้เริ่มทำงาน


224 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

ชุดคำสั่งที่ 9.3 src/com/cookbook/datastorage/DataStorage.java<br />

package com.cookbook.datastorage;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

public class DataStorage extends Activity {<br />

/** Called when the activity is first created. */<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

Intent i = new Intent(this, MyPreferences.class);<br />

startActivity(i);<br />

}<br />

}<br />

ชุดคำสั่งที่ 9.4 จะแสดงข้อมูลภายในไฟล์ Manifest ที่จะใช้งานร่วมกับ PreferenceActivity<br />

ชุดคำสั่งที่ 9.4 AndroidManifest.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />


จากชุดคำสั่งข้างต้นให้หน้าจอ Preferences ดังรูปที่ 9.1<br />

Shared Preferences<br />

225<br />

รูปที่ 9.1 หน้าจอ Preferences ที่สร้างโดยใช้ข้อมูลจากไฟล์ Preferences<br />

กรรมวิธี: การปรับเปลี่ยนส่วนการติดต่อกับผู้ใช้งานโดยอ้างอิงจากข้อมูล<br />

ที่จัดเก็บไว้<br />

แอคทิวิตี้ DataStorage จากในหัวข้อที่แล้วจะถูกนำมาใช้เพื่อตรวจสอบค่าของ Preferences<br />

ที่กำลังอ่าน สำหรับในหัวข้อนี้ ถ้ามีการเก็บข้อมูล Username และ Password ไว้ในไฟล์ Shared-<br />

Preferences อยู่แล้ว ระบบก็จะตรวจสอบดู ถ้าตรงกันก็จะทำงานต่อโดยอัตโนมัติ แต่ถ้าไม่มีข้อมูล<br />

Username และ Password เก็บไว้ในไฟล์ SharedPreferences ระบบก็จะแสดงหน้าจอเพื่อให้<br />

กำหนดค่า Preferences แทน<br />

เราสามารถแก้ไขไฟล์ main.xml เพื่อปรับแต่งหน้าล็อกอินได้ดังแสดงในชุดคำสั่งที่ 9.5 ในที่นี้<br />

เราจะสร้างออบเจ็กต์ EditText ขึ้นมา 2 ตัวเพื่อใช้กรอกข้อมูลของ Username และ Password<br />

ชุดคำสั่งที่ 9.5 res/layout/main.xml<br />

<br />

<br />


226 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

android:text="username"<br />

/><br />

<br />

<br />

<br />

<br />

<br />

แอคทิวิตี้หลัก DataStoragge ที่แสดงในชุดคำสั่งที่ 9.6 มีการแก้ไขเพื่อให้อ่านข้อมูลของ<br />

Username และ Password จากอินสแตนซ์ SharedPreferences ถ้าไม่พบข้อมูลในอินสแตนซ์<br />

แอพจะเรียกใช้แอคทิวิตี้ MyPreferences (ชุดคำสั่งที่ 9.2) เพื่อกำหนดค่า Preferences โดยตรง<br />

แต่ถ้าพบข้อมูลในอินสแตนซ์ แอพจะแสดงเลย์เอาต์ของ main.xml ตามรูปที่ 9.2 ซึ่งปุ่มที่แสดงนั้นจะมี<br />

คำสั่ง onClickListener เพื่อตรวจสอบข้อมูลการล็อกอินว่าตรงกับที่ก ำหนดไว้ใน SharedPreferences<br />

หรือไม่ ถ้าตรงกันก็จะแสดงการทำงานในขั้นตอนต่อไป ในกรณีที่ใส่ข้อมูลล็อกอินไม่ถูกต้อง<br />

แอพก็จะแสดงข้อมูลจากคำสั่ง Toast เพื่อแสดงข้อมูลให้รู้ว่าการล็อกอินผิดพลาด<br />

ชุดคำสั่งที่ 9.6 src/com/cookbook/datastorage/DataStorage.java<br />

package com.cookbook.datastorage;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.content.SharedPreferences;<br />

import android.os.Bundle;<br />

import android.preference.PreferenceManager;


import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

import android.widget.EditText;<br />

import android.widget.Toast;<br />

Shared Preferences<br />

227<br />

public class DataStorage extends Activity {<br />

SharedPreferences myprefs;<br />

EditText userET, passwordET;<br />

Button loginBT;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

myprefs = PreferenceManager.getDefaultSharedPreferences(this);<br />

final String username = myprefs.getString("username", null);<br />

final String password = myprefs.getString("password", null);<br />

if (username != null && password != null){<br />

setContentView(R.layout.main);<br />

userET = (EditText)findViewById(R.id.userText);<br />

passwordET = (EditText)findViewById(R.id.passwordText);<br />

loginBT = (Button)findViewById(R.id.loginButton);<br />

loginBT.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

try {<br />

if(username.equals(userET.getText().toString())<br />

&& password.equals(<br />

passwordET.getText().toString())) {<br />

Toast.makeText(DataStorage.this,<br />

"login passed!!",<br />

Toast.LENGTH_SHORT).show();<br />

Intent i = new Intent(DataStorage.this,<br />

myPreferences.class);<br />

startActivity(i);<br />

} else {<br />

Toast.makeText(DataStorage.this,<br />

"login failed!!",<br />

Toast.LENGTH_SHORT).show();<br />

}<br />

} catch (Exception e) {<br />

e.printStackTrace();<br />

}<br />

}<br />

});<br />

} else {<br />

Intent i = new Intent(this, MyPreferences.class);<br />

startActivity(i);<br />

}<br />

}<br />

}


228 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

รูปที่ 9.2 หน้าล็อกอินจากชุดคำาสั่งที่ 9.5<br />

กรรมวิธี: การเรียกใช้หน้าจอแสดงข้อตกลงลิขสิทธิ์ของผู้ใช้งาน<br />

เรามักจะกำหนดให้ระบบแสดงหน้าจอข้อตกลงลิขสิทธิ์ของผู้ใช้งานในการติดตั้งแอพครั้งแรก<br />

และในระหว่างที่เรียกใช้แอพ ถ้าผู้ใช้ไม่ยอมรับข้อตกลงนั้น ระบบก็จะยกเลิกการติดตั้งหรือหยุดทำงาน<br />

แต่ถ้าผู้ใช้ยอมรับข้อตกลง หน้าจอดังกล่าวก็จะไม่แสดงขึ้นอีก<br />

คำสั่งที่ใช้เปิดหน้าจอแสดงข้อตกลงลิขสิทธิ์ของผู้ใช้งานนั้นสามารถเรียกใช้ได้จากคลาส EULA<br />

ตามที่แสดงในชุดคำสั่งที่ 9.7 ซึ่งจะแสดงการใช้ SharedPreferences ร่วมกับตัวแปรแบบบูลีนชื่อ<br />

PREFERENCE_EULA_ACCEPTED เพื่อตรวจสอบว่าผู้ใช้งานได้ยอมรับในข้อตกลงดังกล่าวหรือไม่<br />

ชุดคำสั่งที่ 9.7 src/com/cookbook/eula_example/Eula.java<br />

/*<br />

* Copyright (C) 2008 The Android Open Source Project<br />

*<br />

* Licensed under the Apache License, Version 2.0 (the "License");<br />

* you may not use this file except in compliance with the License.<br />

* You may obtain a copy of the License at<br />

*<br />

* http://www.apache.org/licenses/LICENSE-2.0<br />

*<br />

* Unless required by applicable law or agreed to in writing, software<br />

* distributed under the License is distributed on an "AS IS" BASIS,<br />

* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.<br />

* See the License for the specific language governing permissions and


* limitations under the License.<br />

*/<br />

Shared Preferences<br />

229<br />

package com.cookbook.eula_example;<br />

import android.app.Activity;<br />

import android.app.AlertDialog;<br />

import android.content.DialogInterface;<br />

import android.content.SharedPreferences;<br />

import java.io.IOException;<br />

import java.io.BufferedReader;<br />

import java.io.InputStreamReader;<br />

import java.io.Closeable;<br />

/**<br />

* Displays an EULA ("End User License Agreement") that the user has to<br />

accept before<br />

* using the application.<br />

*/<br />

class Eula {<br />

private static final String ASSET_EULA = "EULA";<br />

private static final String PREFERENCE_EULA_ACCEPTED = "eula.accepted";<br />

private static final String PREFERENCES_EULA = "eula";<br />

/**<br />

* callback to let the activity know when the user accepts the EULA.<br />

*/<br />

static interface OnEulaAgreedTo {<br />

void onEulaAgreedTo();<br />

}<br />

/**<br />

* Displays the EULA if necessary.<br />

*/<br />

static boolean show(final Activity activity) {<br />

final SharedPreferences preferences =<br />

activity.getSharedPreferences(<br />

PREFERENCES_EULA, Activity.MODE_PRIVATE);<br />

//to test:<br />

// preferences.edit()<br />

// .putBoolean(PREFERENCE_EULA_ACCEPTED, false).commit();


230 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

}<br />

if (!preferences.getBoolean(PREFERENCE_EULA_ACCEPTED, false)) {<br />

final AlertDialog.Builder builder =<br />

new AlertDialog.Builder(activity);<br />

builder.setTitle(R.string.eula_title);<br />

builder.setCancelable(true);<br />

builder.setPositiveButton(R.string.eula_accept,<br />

new DialogInterface.OnClickListener() {<br />

public void onClick(DialogInterface dialog, int which) {<br />

accept(preferences);<br />

if (activity instanceof OnEulaAgreedTo) {<br />

((OnEulaAgreedTo) activity).onEulaAgreedTo();<br />

}<br />

}<br />

});<br />

builder.setNegativeButton(R.string.eula_refuse,<br />

new DialogInterface.OnClickListener() {<br />

public void onClick(DialogInterface dialog, int which) {<br />

refuse(activity);<br />

}<br />

});<br />

builder.setOnCancelListener(<br />

new DialogInterface.OnCancelListener() {<br />

public void onCancel(DialogInterface dialog) {<br />

refuse(activity);<br />

}<br />

});<br />

builder.setMessage(readEula(activity));<br />

builder.create().show();<br />

return false;<br />

}<br />

return true;<br />

private static void accept(SharedPreferences preferences) {<br />

preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED,<br />

true).commit();<br />

}<br />

private static void refuse(Activity activity) {<br />

activity.finish();<br />

}<br />

private static CharSequence readEula(Activity activity) {<br />

BufferedReader in = null;<br />

try {<br />

in = new BufferedReader(new<br />

InputStreamReader(activity.getAssets().open(ASSET_EULA)));


}<br />

String line;<br />

StringBuilder buffer = new StringBuilder();<br />

while ((line = in.readLine()) != null)<br />

buffer.append(line).append(‘\n’);<br />

return buffer;<br />

} catch (IOException e) {<br />

return "";<br />

} finally {<br />

closeStream(in);<br />

}<br />

Shared Preferences<br />

231<br />

}<br />

/**<br />

* Closes the specified stream.<br />

*/<br />

private static void closeStream(Closeable stream) {<br />

if (stream != null) {<br />

try {<br />

stream.close();<br />

} catch (IOException e) {<br />

// Ignore<br />

}<br />

}<br />

}<br />

การใช้งานคลาส EULA จะต้องมีการกำหนดค่าเพิ่มเติมดังนี้<br />

1. ข้อความที่จะใช้ประกาศสิทธิ์ในการใช้งาน โดยจะเก็บไว้ในไฟล์ชื่อ EULA (กำหนดไว้ใน<br />

ตัวแปร ASSET_EULA ในชุดคำสั่งที่ 9.7) และเก็บไว้ในไดเร็กทอรี assets/ ภายใน<br />

โปรเจ็กต์แอนดรอยด์ ซึ่งข้อความนี้จะใช้คำสั่ง readEula() ในการอ่านข้อมูลในไฟล์<br />

2. ข้อความที่จะแสดงในไดอะล็อกบ็อกซ์เพื่อยืนยันสิทธิ์ ซึ่งจะเก็บไว้ในไฟล์รีซอร์สดังแสดง<br />

ไว้ในชุดคำสั่งที่ 9.8<br />

ชุดคำสั่งที่ 9.8 res/values/strings.xml<br />

<br />

<br />

Welcome to MyApp<br />

MyApp<br />

License Agreement<br />

Accept<br />

Don\’t Accept<br />


232 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

จากการกำหนดค่าดังกล่าวนี้ เราสามารถแสดง EULA ได้ด้วยการเรียกใช้คำสั่งในเมธอด<br />

onCreate() ดังนี้<br />

Eula.show(this);<br />

ฐานข้อมูล SQLite<br />

ในกรณีที่ต้องทำงานร่วมกับข้อมูลที่มีขนาดใหญ่ การใช้งาน SQLite เพื่อเก็บข้อมูลดู<br />

จะเหมาะสมมากกว่าการใช้ Shared Preferences หรือไฟล์ข้อความ โดยในระบบปฏิบัติการ<br />

แอนดรอยด์นั้นมีการติดตั้งระบบฐานข้อมูล SQLite มาด้วยแล้ว SQLite มีลักษณะเป็นฐานข้อมูลเชิง<br />

สัมพันธ์ ซึ่งเราสามารถใช้คำสั่ง SQL ในการสืบค้นข้อมูลได้ แอพที่ใช้งาน SQLite นั้นจะมีอินสแตนซ์<br />

ของฐานข้อมูลเป็นของตัวเอง สามารถเข้าถึงได้จากตัวแอพเท่านั้น ไฟล์ฐานข้อมูลจะเก็บไว้ใน<br />

ไดเร็กทอรี /data/data//databases ขั้นตอนในการใช้งาน SQLite มีดังนี้<br />

1. สร้างฐานข้อมูล<br />

2. เปิดฐานข้อมูล<br />

3. สร้างตาราง<br />

4. สร้างอินเตอร์เฟซเพื่อใช้ในการเพิ่มข้อมูล<br />

5. สร้างอินเตอร์เฟซเพื่อใช้ในการค้นหาข้อมูล<br />

6. ปิดฐานข้อมูล<br />

ในหัวข้อถัดไปเราจะกล่าวถึงขั้นตอนของการเขียนชุดคำสั่งเพื่อทำงานดังกล่าว<br />

กรรมวิธี: การสร้างแพ็คเกจฐานข้อมูล<br />

ในโปรเจ็กต์แอนดรอยด์ที่มีความซับซ้อนนั้น เราควรสร้างแพ็คเกจฐานข้อมูลเอาไว้เพื่อให้ใช้งาน<br />

ได้ง่ายและเกิดความเป็นระเบียบ เช่น การนำคลาส database มาใส่ไว้ในแพ็คเกจ com.cookbook.<br />

data จะช่วยให้ง่ายต่อการเรียกใช้ในภายหลัง ซึ่งแพ็คเกจฐานข้อมูลจะประกอบไปด้วยคลาสจำนวน<br />

3 คลาส ดังนี้<br />

m MyDB() – ใช้เริ่มการทำงานของ MyDBhelper<br />

m open() – ใช้เริ่มการทำงานของอินสแตนซ์ SQLiteDatabase ด้วยการเรียกใช้<br />

MyDBHelper ซึ่งคำสั่งนี้จะใช้เปิดการเชื่อมต่อกับฐานข้อมูลแบบเขียนได้ แต่ในกรณีที่<br />

ระบบพบปัญหาในการทำงาน คำสั่งที่จะใช้ในการเปิดการเชื่อมต่อกับฐานข้อมูลจะเปลี่ยน<br />

เป็นแบบอ่านได้เพียงอย่างเดียว<br />

m close() – ปิดการเชื่อมต่อกับฐานข้อมูล<br />

m insertdiary() – ใช้เก็บข้อมูลในรูปแบบรายการลงในอินสแตนซ์ ContentValues<br />

และส่งค่าดังกล่าวไปยังอินสแตนซ์ SQLitedatabase เพื่อเพิ่มข้อมูล<br />

m getdiaries() – ใช้ในการอ่านข้อมูลจากฐานข้อมูล และเก็บข้อมูลดังกล่าวไว้ในคลาส<br />

Cursor และส่งค่ากลับไปยังเมธอด


ชุดคำสั่งที่ 9.9 src/com/cookbook/data/MyDB.java<br />

ฐานข้อมูล SQLite<br />

233<br />

package com.cookbook.data;<br />

import android.content.ContentValues;<br />

import android.content.Context;<br />

import android.database.Cursor;<br />

import android.database.sqlite.SQLiteDatabase;<br />

import android.database.sqlite.SQLiteException;<br />

import android.util.Log;<br />

public class MyDB {<br />

private SQLiteDatabase db;<br />

private final Context context;<br />

private final MyDBhelper dbhelper;<br />

public MyDB(Context c){<br />

context = c;<br />

dbhelper = new MyDBhelper(context, Constants.DATABASE_NAME, null,<br />

Constants.DATABASE_VERSION);<br />

}<br />

public void close()<br />

{<br />

db.close();<br />

}<br />

public void open() throws SQLiteException<br />

{<br />

try {<br />

db = dbhelper.getWritableDatabase();<br />

} catch(SQLiteException ex) {<br />

Log.v("Open database exception caught", ex.getMessage());<br />

db = dbhelper.getReadableDatabase();<br />

}<br />

}<br />

public long insertdiary(String title, String content)<br />

{<br />

try{<br />

ContentValues newTaskValue = new ContentValues();<br />

newTaskValue.put(Constants.TITLE_NAME, title);<br />

newTaskValue.put(Constants.CONTENT_NAME, content);<br />

newTaskValue.put(Constants.DATE_NAME,<br />

java.lang.System.currentTimeMillis());<br />

return db.insert(Constants.TABLE_NAME, null, newTaskValue);<br />

} catch(SQLiteException ex) {


234 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

}<br />

Log.v("Insert into database exception caught",<br />

ex.getMessage());<br />

return -1;<br />

}<br />

}<br />

public Cursor getdiaries()<br />

{<br />

Cursor c = db.query(Constants.TABLE_NAME, null, null,<br />

null, null, null, null);<br />

return c;<br />

}<br />

ในชุดคำสั่งที่ 9.10 จะแสดงการสร้างคลาส MyDBhelper เอาไว้ ซึ่งเฟรมเวิร์ค SQLIteOpen-<br />

Helper มีเมธอดที่ใช้จัดการหรืออัพเกรดฐานข้อมูลอยู่ เมื่อเริ่มใช้งานฐานข้อมูล เราจะต้องกำหนดชื่อ<br />

ของฐานข้อมูลที่ต้องการสร้าง โดยจะเก็บไว้ใน /data/data/com.cookbook.datastorage/<br />

databases และกำหนดค่าของเวอร์ชั่นฐานข้อมูลเพื่อที่ระบบจะได้ตรวจสอบได้ว่าจะต้องเรียกใช้คำสั่ง<br />

onCreate() หรือคำสั่ง onUpgrade()<br />

เราสามารถสร้างตารางในฐานข้อมูลได้ด้วยการใช้คำสั่ง SQL ซึ่งจะเขียนไว้ในเมธอด onCreate()<br />

ดังนี้<br />

create table MyTable (key_id integer primary key autoincrement,<br />

title text not null, content text not null,<br />

recorddate long);<br />

ในกรณีที่มีการอัพเกรดฐานข้อมูล เมื่ออัพเกรดเสร็จแล้ว ค่าตัวเลขของเวอร์ชั่นฐานข้อมูลจะ<br />

เปลี่ยนไป<br />

ชุดคำสั่งที่ 9.10 src/com/cookbook/data/MyDBhelper.java<br />

package com.cookbook.data;<br />

import android.content.Context;<br />

import android.database.sqlite.SQLiteDatabase;<br />

import android.database.sqlite.SQLiteException;<br />

import android.database.sqlite.SQLiteOpenHelper;<br />

import android.database.sqlite.SQLiteDatabase.CursorFactory;<br />

import android.util.Log;<br />

public class MyDBhelper extends SQLiteOpenHelper{<br />

private static final String CREATE_TABLE="create table "+<br />

Constants.TABLE_NAME+" ("+<br />

Constants.KEY_ID+" integer primary key autoincrement, "+<br />

Constants.TITLE_NAME+" text not null, "+<br />

Constants.CONTENT_NAME+" text not null, "+<br />

Constants.DATE_NAME+" long);";


ฐานข้อมูล SQLite<br />

235<br />

public MyDBhelper(Context context, String name, CursorFactory factory,<br />

int version) {<br />

super(context, name, factory, version);<br />

}<br />

@Override<br />

public void onCreate(SQLiteDatabase db) {<br />

Log.v("MyDBhelper onCreate","Creating all the tables");<br />

try {<br />

db.execSQL(CREATE_TABLE);<br />

} catch(SQLiteException ex) {<br />

Log.v("Create table exception", ex.getMessage());<br />

}<br />

}<br />

}<br />

@Override<br />

public void onUpgrade(SQLiteDatabase db, int oldVersion,<br />

int newVersion) {<br />

Log.w("TaskDBAdapter", "Upgrading from version "+oldVersion<br />

+" to "+newVersion<br />

+", which will destroy all old data");<br />

db.execSQL("drop table if exists "+Constants.TABLE_NAME);<br />

onCreate(db);<br />

}<br />

ไฟล์ที่ 3 ของแพ็คเกจ com.cookbook.data คือคลาส Constants ตามที่แสดงในชุดคำสั่งที่<br />

9.11 คลาส Constants นี้จะใช้เก็บค่าคงที่ต่างๆ ที่ใช้ใน MyDB และ MyDBhelper<br />

ชุดคำสั่งที่ 9.11 src/com/cookbook/data/Constants.java<br />

package com.cookbook.data;<br />

public class Constants {<br />

public static final String DATABASE_NAME="datastorage";<br />

public static final int DATABASE_VERSION=1;<br />

public static final String TABLE_NAME="diaries";<br />

public static final String TITLE_NAME="title";<br />

public static final String CONTENT_NAME="content";<br />

public static final String DATE_NAME="recorddate";<br />

public static final String KEY_ID="_id";<br />

}


236 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

กรรมวิธี: การใช้งานแพ็คเกจฐานข้อมูล<br />

หัวข้อนี้จะแสดงการใช้แพ็คเกจฐานข้อมูลที่ได้สร้างไว้ก่อนหน้านี้ โดยเราจะนำหน้าจอล็อกอินที่<br />

ได้สร้างไว้ในหัวข้อก่อนๆ มาสร้างและแสดงรายการข้อมูล ในขั้นตอนแรกนี้เราจะกำหนดเลย์เอาต์ที่จะ<br />

ใช้ในการแสดงข้อมูลชื่อ diary.xml ก่อน ดังแสดงในชุดคำสั่งที่ 9.12 และรูปที่ 9.3<br />

ชุดคำสั่งที่ 9.12 res/layout/diary.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />


ฐานข้อมูล SQLite<br />

237<br />

รูปที่ 9.3 หน้าจอสำาหรับกรอกข้อมูลจากเลย์เอาต์ diary.xml<br />

ในชุดคำสั่งที่ 9.13 จะแสดงการทำงานของแอคทิวิตี้หลัก Diary.java ซึ่งจะนำแพ็คเกจชื่อ<br />

com.cookbook.data เข้ามา และประกาศออบเจ็กต์ MyDB เพื่อเปิดการใช้งานฐานข้อมูล รวมทั้ง<br />

แสดงเลย์เอาต์จากไฟล์ diary.xml เพื่อใช้ในการรับข้อมูลแล้วเก็บลงฐานข้อมูล<br />

ชุดคำสั่งที่ 9.13 src/com/cookbook/datastorage/Diary.java<br />

package com.cookbook.datastorage;<br />

import android.app.Activity;<br />

import android.content.Intent;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

import android.widget.EditText;<br />

import com.cookbook.data.MyDB;<br />

public class Diary extends Activity {<br />

EditText titleET, contentET;<br />

Button submitBT;<br />

MyDB dba;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {


}<br />

238 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.diary);<br />

dba = new MyDB(this);<br />

dba.open();<br />

titleET = (EditText)findViewById(R.id.diarydescriptionText);<br />

contentET = (EditText)findViewById(R.id.diarycontentText);<br />

submitBT = (Button)findViewById(R.id.submitButton);<br />

submitBT.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

try {<br />

saveItToDB();<br />

} catch (Exception e) {<br />

e.printStackTrace();<br />

}<br />

}<br />

});<br />

}<br />

public void saveItToDB() {<br />

dba.insertdiary(titleET.getText().toString(),<br />

contentET.getText().toString());<br />

dba.close();<br />

titleET.setText("");<br />

contentET.setText("");<br />

Intent i = new Intent(Diary.this, DisplayDiaries.class);<br />

startActivity(i);<br />

}<br />

คลาส DataStorage.java จะมีการทำงานเช่นเดียวกับ MyPreferences.class ซึ่งจะสั่งให้<br />

คลาส Diary.class เริ่มทำงานเมื่อล็อกอินสำเร็จ<br />

Toast.makeText(DataStorage.this, “login passed!!”,<br />

Toast.LENGTH_SHORT).show();<br />

Intent i = new Intent(DataStorage.this, Diary.class);<br />

startActivity(i);<br />

ในขั้นตอนสุดท้ายเป็นการแก้ไขไฟล์ Manifest เพื่อเพิ่มแอคทิวิตี้ใหม่เข้าไป ดังแสดงในชุด<br />

คำสั่งที่ 9.14<br />

ชุดคำสั่งที่ 9.14 AndroidManifest.xml<br />

<br />

<br />


ฐานข้อมูล SQLite<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

239<br />

จะเห็นว่าฐานข้อมูลได้ถูกรวมเข้าไว้ในเลย์เอาต์ สำหรับในหัวข้อถัดไปเราจะมาเพิ่มชุดคำสั่งเพื่อ<br />

แสดงรายการข้อมูลกัน<br />

กรรมวิธี: การสร้างไดอารีส่วนตัว<br />

ในหัวข้อนี้เราจะใช้ออบเจ็กต์ ListView ในการแสดงรายการข้อมูลจากตารางในฐานข้อมูล<br />

SQLite โดยจะแสดงรายการข้อมูลในแนวดิ่ง สามารถเลื่อนจอขึ้นลงได้ และจะสร้างไฟล์ XML จำนวน<br />

2 ไฟล์ คือ diaries.xml เพื่อใช้แสดงใน ListView ตามที่แสดงไว้ในชุดคำสั่งที่ 9.15 และไฟล์<br />

diaryrow.xml เพื่อสร้างแถวของข้อมูล ตามชุดคำสั่งที่ 9.16<br />

ชุดคำสั่งที่ 9.15 res/layout/diaries.xml<br />

<br />

<br />


240 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

<br />

<br />

<br />

แอคทิวิตี้ DisplayDiaries.java จะใช้ในการแสดงรายการของ ListView ภายในคลาสนี้จะมี<br />

การประกาศคลาสย่อยอีก 2 ตัว คือ MyDiary ซึ่งใช้ในการเก็บข้อมูลของบันทึก และคลาส<br />

DiaryAdapter เพื่อใช้เก็บข้อมูลที่อ่านมาจากฐานข้อมูล (ใช้คำสั่ง getdata()) โดยมีเมธอดที่เรียก<br />

ใช้งานได้ดังนี้<br />

m getCount() – ส่งค่ากลับเป็นจำนวนของรายการที่ได้จากฐานข้อมูล<br />

m getItem() – ส่งค่ากลับเป็นรายการที่กำหนดไว้<br />

m getItemID() – ส่งค่ากลับเป็นเลข ID ที่ได้จากรายการ<br />

m getView() – จะส่งค่ากลับเป็นค่าของวิวในแต่ละรายการ<br />

ListView จะเรียกใช้ getView() เพื่อแสดงวิวของแต่ละรายการ ถ้าอยากเพิ่มประสิทธิภาพ<br />

ในการแสดงผลละก็ ให้เรียกใช้วิวที่ได้จาก getView() มาใช้งานซ้ำให้มากที่สุดเท่าที่จะทำได้ ด้วยการ<br />

ใช้คลาส ViewHolder มารองรับการแสดงวิวนี้<br />

เมื่อคำสั่ง getView() ถูกเรียกใช้ วิวที่แสดงในขณะนั้นจะถูกใช้เพื่อแสดงรายการข้อมูล<br />

ซึ่งทำงานภายใต้คลาส ViewHolder และเมื่อรายการข้อมูลมีการเปลี่ยนแปลง ระบบก็แสดงรายการ<br />

ข้อมูลใหม่ในวิวเดิม แทนที่จะสร้างวิวใหม่<br />

ชุดคำสั่งที่ 9.17 แสดงการทำงานของแอคทิวิตี้หลัก ซึ่งจะแสดงผลการทำงานของ ListView<br />

ดังแสดงในรูปที่ 9.4<br />

ชุดคำสั่งที่ 9.17 src/com/cookbook/datastorage/DisplayDiaries.java<br />

package com.cookbook.datastorage;<br />

import java.text.DateFormat;<br />

import java.util.ArrayList;<br />

import java.util.Date;<br />

import android.app.ListActivity;<br />

import android.content.Context;<br />

import android.database.Cursor;<br />

import android.os.Bundle;<br />

import android.view.LayoutInflater;<br />

import android.view.View;


ฐานข้อมูล SQLite<br />

241<br />

import android.view.ViewGroup;<br />

import android.widget.BaseAdapter;<br />

import android.widget.TextView;<br />

import com.cookbook.data.Constants;<br />

import com.cookbook.data.MyDB;<br />

public class DisplayDiaries extends ListActivity {<br />

MyDB dba;<br />

DiaryAdapter myAdapter;<br />

private class MyDiary{<br />

public MyDiary(String t, String c, String r){<br />

title=t;<br />

content=c;<br />

recorddate=r;<br />

}<br />

public String title;<br />

public String content;<br />

public String recorddate;<br />

}<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

dba = new MyDB(this);<br />

dba.open();<br />

setContentView(R.layout.diaries);<br />

}<br />

super.onCreate(savedInstanceState);<br />

myAdapter = new DiaryAdapter(this);<br />

this.setListAdapter(myAdapter);<br />

private class DiaryAdapter extends BaseAdapter {<br />

private LayoutInflater mInflater;<br />

private ArrayList diaries;<br />

public DiaryAdapter(Context context) {<br />

mInflater = LayoutInflater.from(context);<br />

diaries = new ArrayList();<br />

getdata();<br />

}<br />

public void getdata(){<br />

Cursor c = dba.getdiaries();<br />

startManagingCursor(c);<br />

if(c.moveToFirst()){<br />

do{<br />

String title =<br />

c.getString(c.getColumnIndex(Constants.TITLE_NAME));


242 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

}<br />

}<br />

String content =<br />

c.getString(c.getColumnIndex(Constants.CONTENT_NAME));<br />

DateFormat dateFormat =<br />

DateFormat.getDateTimeInstance();<br />

String datedata = dateFormat.format(new<br />

Date(c.getLong(c.getColumnIndex(<br />

Constants.DATE_NAME))).getTime());<br />

MyDiary temp = new MyDiary(title,content,datedata);<br />

diaries.add(temp);<br />

} while(c.moveToNext());<br />

@Override<br />

public int getCount() {return diaries.size();}<br />

public MyDiary getItem(int i) {return diaries.get(i);}<br />

public long getItemId(int i) {return i;}<br />

public View getView(int arg0, View arg1, ViewGroup arg2) {<br />

final ViewHolder holder;<br />

View v = arg1;<br />

if ((v == null) || (v.getTag() == null)) {<br />

v = mInflater.inflate(R.layout.diaryrow, null);<br />

holder = new ViewHolder();<br />

holder.mTitle = (TextView)v.findViewById(R.id.name);<br />

holder.mDate = (TextView)v.findViewById(R.id.datetext);<br />

v.setTag(holder);<br />

} else {<br />

holder = (ViewHolder) v.getTag();<br />

}<br />

holder.mdiary = getItem(arg0);<br />

holder.mTitle.setText(holder.mdiary.title);<br />

holder.mDate.setText(holder.mdiary.recorddate);<br />

v.setTag(holder);<br />

}<br />

return v;<br />

}<br />

}<br />

public class ViewHolder {<br />

MyDiary mdiary;<br />

TextView mTitle;<br />

TextView mDate;<br />

}


Content Provider<br />

243<br />

รูปที่ 9.4 รายการข้อมูลที่แสดงใน ListView<br />

Content Provider<br />

แอพแต่ละตัวจะทำงานภายในพื้นที่ของตนเองซึ่งแอพอื่นๆ จะไม่สามารถเข้าถึงได้ ถ้าต้องการ<br />

เข้าถึงการทำงานเหล่านั้นละก็ เราต้องประกาศสิทธิ์ในการใช้งานแอพในระหว่างที่ติดตั้งเสียก่อน ระบบ<br />

ปฏิบัติการแอนดรอยด์มีการสร้างอินเตอร์เฟซเพื่อใช้ในการทำงานนี้ชื่อ ContentProvider มีลักษณะ<br />

การทำงานเหมือนเป็นสะพานที่เชื่อมโยงแอพเข้าด้วยกัน โดยจะแยกเป็นชั้นของแอพและชั้นของข้อมูล<br />

ในการใช้งานคำสั่งเหล่านี้จำเป็นต้องกำหนดสิทธิ์ในการใช้งานลงไปในไฟล์ Manifest ด้วยเพื่อให้เข้าถึง<br />

ข้อมูลดังกล่าวด้วยคำสั่ง URI ได้<br />

มีฐานข้อมูลในแอนดรอยด์บางตัวที่สามารถทำงานร่วมกับ ContentProvider ได้ ตัวอย่างเช่น<br />

m Browser – อ่านและแก้ไขบุ๊คมาร์ค, ประวัติการใช้งาน, คำค้นต่างๆ<br />

m CallLog – อ่านและแก้ไขประวัติการโทร<br />

m Contacts – อ่านและแก้ไขสมุดโทรศัพท์ ซึ่งข้อมูลสมุดโทรศัพท์นั้นจะเก็บอยู่ในรูปแบบ<br />

tree-tier ดังนี้<br />

m ContactsContract.Data – เก็บข้อมูลส่วนตัวทุกประเภท เช่น เบอร์โทรศัพท์,<br />

อีเมล์แอดเดรส เป็นต้น<br />

m ContactsContract.RawContacts – เก็บกลุ่มของข้อมูลที่สัมพันธ์กับบัญชี<br />

รายชื่อผู้ใช้งาน<br />

m ContactsContract.Contacts – เก็บข้อมูลของ RawContacts ที่มีความ<br />

สัมพันธ์กัน


244 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

m LiveFolders – โฟลเดอร์พิเศษที่สร้างโดย ContentProvider<br />

m MediaStore – เก็บข้อมูลเสียง, วิดีโอ และรูปภาพ<br />

m Setting – อ่านค่าการใช้งานบลูทูธ, เสียงเรียกเข้า และการเชื่อมต่อกับอุปกรณ์อื่นๆ<br />

m SearchRecentSuggestions – ควบคุมการทำงานของการแสดงคำค้นที่ใกล้เคียง<br />

m SyncStateContract – ควบคุมการทำงานของการซิงค์ข้อมูลกับ Data Provider<br />

m UserDictionary – ควบคุมการทำงานของรายการข้อมูลของคำต่างๆ ที่ผู้ใช้งานกำหนด<br />

ขึ้นเองในระหว่างที่กรอกข้อมูล เช่น ข้อความที่ใช้งานบ่อยๆ เป็นต้น<br />

การเข้าถึงการทำงานของ ContentProvider เราจะต้องสร้างอินสแตนซ์ของคลาส content<br />

Resolver ขึ้นมาเพื่อค้น, เพิ่ม, แก้ไข หรือลบข้อมูลจาก ContentProvider ดังข้างล่างนี้<br />

ContentResolver crInstance = getContentResolver(); //get a content Resolver<br />

instance<br />

crInstance.query(People.CONTENT_URI, null, null, null, null); //query contacts<br />

ContentValues new_Values= new ContentValues();<br />

crInstance.insert(People.CONTENT_URI, new_Values); // insert new values<br />

crInstance.delete(People_URI, null, null); //delete all contacts<br />

ContentValues update_Values= new ContentValues();<br />

crInstance.update(People_URI, update_Value, null,null); //update values<br />

ContentProvider แต่ละตัวจะมีค่า URI (Uniform Resource Identifier) ที่จะใช้ในการ<br />

ประกาศและกำหนดสิทธิ์ในการใช้งาน การกำหนด URI จะต้องมีค่าไม่ซ้ำกัน และจะต้องมีรูปแบบดังนี้<br />

content://.provider./<br />

ในหัวข้อนี้เราสามารถใช้ค่า URI เป็น content://com.cookbook.datastorage/diaries<br />

ได้ ซึ่งจะถูกหยิบมาใช้ในหัวข้อถัดไป นอกจากนี้ยังสามารถตรวจสอบได้ด้วยว่า URI ที่กำหนดไว้ถูกต้อง<br />

หรือไม่ด้วยการใช้คำสั่ง Urimatcher ของ ContentProvider<br />

กรรมวิธี: การสร้าง Content Provider ขึ้นเอง<br />

หลังจากที่เข้าใจการทำงานของการใช้งาน ContentProvider แล้ว ในหัวข้อนี้เราจะเพิ่ม<br />

Content Provider ลงไปในโปรเจ็กต์ โดยจะแสดงวิธีการนำข้อมูลจากแอพที่เราสร้างขึ้นไปแสดงบน<br />

แอพอื่นๆ เราจะสร้าง Content Provider ขึ้นมาเอง โดยมีเมธอดอยู่ 6 ตัวที่ต้องทำการโอเวอร์ไรด์


Content Provider<br />

m query() – อนุญาตให้แอพอื่นอ่านข้อมูลได้<br />

m insert() – อนุญาตให้แอพอื่นเพิ่มข้อมูลได้<br />

m update() – อนุญาตให้แอพอื่นแก้ไขข้อมูลได้<br />

m delete() – อนุญาตให้แอพอื่นลบข้อมูลได้<br />

m getType() – อนุญาตให้แอพอื่นดูรายการ URI ที่ใช้งานได้<br />

m onCreate() – อนุญาตให้แอพอื่นสร้างฐานข้อมูลเพื่อแสดงข้อมูลได้<br />

245<br />

ตัวอย่างเช่น ถ้าเราต้องการให้แอพอื่นๆ สามารถเข้ามาอ่านข้อมูลจากในแอพของเราได้ เราก็จะ<br />

ต้องโอเวอร์ไรด์ คำสั่ง onCreate() และ query()<br />

ชุดคำสั่งที่ 9.18 แสดงถึงการสร้าง ContentProvider ด้วยตัวเอง โดยได้กำหนด URI ไว้ 1<br />

ตัวให้แก่ UriMatcher ซึ่งอ้างอิงจากแพ็คเกจ com.cookbook.datastorage และฐานข้อมูลชื่อ<br />

diaries โดยคำสั่ง query() จะอ่านข้อมูลจากฐานข้อมูลที่จะถูกเรียกใช้ด้วยคำสั่ง URI<br />

ชุดคำสั่งที่ 9.18 src/com/cookbook/datastorage/DiaryContentProvider.java<br />

package com.cookbook.datastorage;<br />

import android.content.ContentProvider;<br />

import android.content.ContentValues;<br />

import android.content.UriMatcher;<br />

import android.database.Cursor;<br />

import android.database.sqlite.SQLiteQueryBuilder;<br />

import android.net.Uri;<br />

import com.cookbook.data.Constants;<br />

import com.cookbook.data.MyDB;<br />

public class DiaryContentProvider extends ContentProvider {<br />

private MyDB dba;<br />

private static final UriMatcher sUriMatcher;<br />

//the code returned for URI match to components<br />

private static final int DIARIES=1;<br />

public static final String AUTHORITY = "com.cookbook.datastorage";<br />

static {<br />

sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);<br />

sUriMatcher.addURI(AUTHORITY, Constants.TABLE_NAME,<br />

DIARIES);<br />

}<br />

@Override


246 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

public int delete(Uri uri, String selection, String[] selectionArgs) {<br />

return 0;<br />

}<br />

public String getType(Uri uri) {return null;}<br />

public Uri insert(Uri uri, ContentValues values) {return null;}<br />

public int update(Uri uri, ContentValues values, String selection,<br />

String[] selectionArgs) {return 0;}<br />

@Override<br />

public boolean onCreate() {<br />

dba = new MyDB(this.getContext());<br />

dba.open();<br />

return false;<br />

}<br />

}<br />

@Override<br />

public Cursor query(Uri uri, String[] projection, String selection,<br />

String[] selectionArgs, String sortOrder) {<br />

Cursor c=null;<br />

switch (sUriMatcher.match(uri)) {<br />

case DIARIES:<br />

c = dba.getdiaries();<br />

break;<br />

default:<br />

throw new IllegalArgumentException(<br />

"Unknown URI " + uri);<br />

}<br />

c.setNotificationUri(getContext().getContentResolver(), uri);<br />

return c;<br />

}<br />

ในชุดคำสั่งที่ 9.19 จะแสดงการกำหนด ContentProvider ลงในไฟล์ Manifest เพื่อกำหนด<br />

สิทธิ์ในการเข้าใช้งาน<br />

ชุดคำสั่งที่ 9.19 AndroidManifest.xml<br />

<br />

<br />


Content Provider<br />

247<br />

android:label="@string/app_name"><br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

หลังจากนี้ ContentProvider ที่เราสร้างขึ้นก็จะอยู่ในรูปแบบที่สามารถเรียกใช้งานได้แล้ว<br />

ในการทดสอบการทำงานของ ContentProvider นี้ เราจะสร้างโปรเจ็กต์แอนดรอยด์ขึ้นมาใหม่ชื่อ<br />

DataStorageTester และกำหนดให้แอคทิวิตี้หลักชื่อ DataStorageTester ตามที่แสดงในชุดคำสั่ง<br />

ที่ 9.20 ซึ่งอินสแตนซ์ของคลาส ContentResult จะใช้ในการค้นหาข้อมูลจาก ContentProvider<br />

ชื่อ DataStorage และเมื่อการเรียกใช้ URI เสร็จสิ้น ก็จะแสดงข้อมูลที่ได้บนจอโดยใช้ออบเจ็กต์<br />

StringBuilder<br />

ชุดคำสั่งที่ 9.20 src/com/cookbook/datastorage_tester/DataStorageTester.java<br />

package com.cookbook.datastorage_tester;<br />

import android.app.Activity;<br />

import android.content.ContentResolver;<br />

import android.database.Cursor;<br />

import android.net.Uri;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

public class DataStorageTester extends Activity {<br />

TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

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


248 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

String myUri = "content://com.cookbook.datastorage/diaries";<br />

Uri CONTENT_URI = Uri.parse(myUri);<br />

//get ContentResolver instance<br />

ContentResolver crInstance = getContentResolver();<br />

Cursor c = crInstance.query(CONTENT_URI, null, null, null, null);<br />

startManagingCursor(c);<br />

StringBuilder sb = new StringBuilder();<br />

if(c.moveToFirst()){<br />

do{<br />

sb.append(c.getString(1)).append("\n");<br />

}while(c.moveToNext());<br />

}<br />

tv.setText(sb.toString());<br />

}<br />

}<br />

ชุดคำสั่งที่ 9.21 จะแสดงเลย์เอาต์ของไฟล์ main.xml และกำหนดค่า ID เพื่อแสดงใน<br />

ออบเจ็กต์ TextView<br />

ชุดคำสั่งที่ 9.21 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

ผลลัพธ์ของการทำงานชุดคำสั่งนี้จะแสดงอยู่ในรูปที่ 9.5


การจัดเก็บและการเปิดไฟล์<br />

249<br />

รูปที่ 9.5 ผลลัพธ์จากการใช้คำาสั่ง ContentProvider<br />

ในการอ่านข้อมูลจากแอพอื่นๆ<br />

การจัดเก็บและการเปิดไฟล์<br />

ในการจัดเก็บและการเปิดไฟล์ในระบบปฏิบัติการแอนดรอยด์นั้น เราจะใช้แพ็คเกจ java.io.File<br />

ในการทำงาน แพ็คเกจนี้จะมีคำสั่งที่ใช้ในการจัดการไฟล์ประเภทข้อความอยู่ เช่น FileInput-<br />

Stream, FileOutputStream, InputStream และ OutputStream ตัวอย่างการใช้งานคำสั่งมีดังนี้<br />

FileInputStream fis = openFileInput(“myfile.txt”);<br />

FileOutputStream fos = openFileOutput(“myfile.txt”,<br />

Context.MODE_WORLD_WRITABLE);<br />

นี่คืออีกหนึ่งตัวอย่างของการจัดเก็บรูปภาพจากกล้องถ่ายภาพให้อยู่ในฟอร์แมต PNG<br />

Bitmap takenPicture;<br />

FileOutputStream out = openFileOutput(“mypic.png”,<br />

Context.MODE_WORLD_WRITEABLE);<br />

takenPicture.compress(CompressFormat.PNG, 100, out);<br />

out.flush();<br />

out.close();


250 บทที่ 9 การทำางานร่วมกับข้อมูล<br />

เราสามารถอ่านข้อมูลในไฟล์ที่เก็บอยู่ในไดเร็กทอรีรีซอร์สได้ด้วย อย่างเช่นถ้าเราต้องการเปิด<br />

ไฟล์ myrawfile.txt ซึ่งอยู่ในไดเร็กทอรี res/raw ก็จะใช้คำสั่งดังนี้<br />

InputStream is = this.getResource()<br />

.openRawResource(R.raw.myrawfile.txt);


251<br />

บทที่ 10<br />

การระบุตำแหน่ง<br />

แอพสมัยนี้มีการระบุตำแหน่งที่หลากหลาย มีการนำไปรวมเข้ากับฟังก์ชั่นต่างๆ อย่างเช่น<br />

การค้นหาข้อมูล, การถ่ายรูป, เกมต่างๆ หรือแม้แต่แอพประเภทเครือข่ายทางสังคม โดยผู้พัฒนา<br />

สามารถนำเอาคุณสมบัติการทำงานนี้มาผนวกเข้ากับแอพของตัวเองเพื่อเพิ่มประสิทธิภาพในการ<br />

ทำงานได้ด้วย<br />

ในบทนี้จะว่าด้วยชุดคำสั่งที่ใช้ในการการระบุตำแหน่งของอุปกรณ์ และตรวจสอบความ<br />

เปลี่ยนแปลงของตำแหน่งปัจจุบัน รวมทั้งนำเอาพิกัดดังกล่าวไปใช้ระบุตำแหน่งบนแผนที่ด้วย<br />

ขั้นตอนพื้นฐานของการระบุตำแหน่ง<br />

การใช้ระบบการระบุตำแหน่งนั้น เราจะใช้คอมโพเน็นต์ของระบบปฏิบัติการแอนดรอยด์ ดังนี้<br />

m LocationManager – เป็นคลาสที่ใช้เข้าถึงการทำงานของการระบุตำแหน่งในอุปกรณ์<br />

แอนดรอยด์<br />

m LocationListener – เป็นอินเตอร์เฟซที่ใช้ในการรับข้อมูลจาก LocationManager<br />

เมื่อมีการเปลี่ยนตำแหน่ง<br />

m Location – เป็นคลาสที่ใช้แสดงพิกัดทางภูมิศาสตร์<br />

ในการใช้งาน LocationManager นั้น เราต้องเรียกใช้เซอร์วิสชื่อ LOCATION_SERVICE ซึ่งจะ<br />

ส่งค่ากลับเป็นตำแหน่งพิกัดของอุปกรณ์ ทำให้แอพที่ใช้เซอร์วิสนี้สามารถระบุตำแหน่งของอุปกรณ์<br />

และบอกทิศทางการเคลื่อนที่ รวมถึงกำหนดให้แจ้งเตือนเมื่ออุปกรณ์ดังกล่าวออกนอกพื้นที่ที่กำหนดไว้<br />

ได้ด้วย ตัวอย่างของการสั่งให้ LocationManager เริ่มทำงานมีดังนี้<br />

LocationManager mLocationManager;<br />

mLocationManager = (LocationManager)<br />

getSystemService(Context.LOCATION_SERVICE);<br />

หลังจากที่อินสแตนซ์ LocationManager เริ่มทำงานแล้ว เราจะต้องกำหนดเทคโนโลยีที่จะใช้<br />

ในการระบุตำแหน่ง เช่น AGPS, Wi-Fi หรือเครือข่ายโทรศัพท์ ซึ่งการเลือกใช้วิธีการระบุตำแหน่งนั้น<br />

จะส่งผลด้านความถูกต้องแม่นยำและอัตราการสิ้นเปลืองพลังงาน การทำงานประเภทนี้ให้ใช้คลาส<br />

Criteria ที่อยู่ในแพ็คเกจ android.location.Criteria ในการเลือกใช้วิธีการระบุตำแหน่งที่<br />

เหมาะสมในสถานะการใช้งานขณะนั้น ตัวอย่างของการใช้คำสั่ง Criteria มีดังนี้


252 บทที่ 10 การระบุตำาแหน่ง<br />

Criteria criteria = new Criteria();<br />

criteria.setAccuracy(Criteria.ACCURACY_FINE);<br />

criteria.setPowerRequirement(Criteria.POWER_LOW);<br />

String locationprovider =<br />

mLocationManager.getBestProvider(criteria, true);<br />

เทคโนโลยีที่ใช้ในการระบุตำแหน่งส่วนใหญ่จะมีใช้กันอยู่ 2 แบบด้วยกัน โดยเราสามารถใช้<br />

เมธอด getProvider() เพื่อเลือกใช้วิธีที่ต้องการได้ แบบแรกจะเป็นการใช้สัญญาณจากดาวเทียม<br />

หรือที่เรียกว่า GPS (Global Positioning System) โดยกำหนดค่า Provider เป็น Location<br />

Manager.GPS_PROVIDER และแบบที่ 2 จะเป็นการระบุตำแหน่งโดยใช้ค่าจากเครือข่ายโทรศัพท์<br />

โดยระบุตำแหน่งจากที่ตั้งของเสาสัญญาณที่อยู่ใกล้ที่สุด แบบนี้ให้กำหนดค่า Provider เป็น<br />

LocationManager.NETWORK_PROVIDER ซึ่งการรับสัญญาณจากดาวเทียมจะมีความแม่นยำมากกว่า<br />

การระบุตำแหน่งด้วยเครือข่าย แต่มีข้อเสียตรงที่เราไม่สามารถใช้GPS ในที่ร่มหรือที่ที่ไม่สามารถเห็นท้องฟ้าได้<br />

ตัวอย่างการทำงานทั้งหมดในบทนี้ เราจะใช้ไฟล์ 2 ไฟล์มาร่วมทำงานด้วยเสมอ ไฟล์แรกคือ<br />

ไฟล์ main.xml ใช้แสดงเลย์เอาต์หลักเพื่อแสดงข้อมูลพิกัดเหมือนในชุดคำสั่งที่ 10.1<br />

ชุดคำสั่งที่ 10.1 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

ไฟล์ที่ 2 คือไฟล์ AndroidManifest.xml ใช้ในการกำหนดสิทธิ์การใช้งานและคุณสมบัติในการ<br />

ทำงานของแอพ ตามที่แสดงไว้ในชุดคำสั่งที่ 10.2 และเพื่อให้การระบุตำแหน่งมีความแม่นยำมากขึ้น<br />

เราได้เพิ่มคำสั่ง ACCESS_FINE_LOCATION แทนการใช้คำสั่ง ACCESS_COARSE_LOCATION<br />

ชุดคำสั่งที่ 10.2 AndroidManifest.xml<br />

<br />

<br />


<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

253<br />

กรรมวิธี: การแสดงตำแหน่งล่าสุด<br />

เนื่องจากการระบุตำแหน่งนั้นจะต้องใช้ระยะเวลาในการทำงานสักครู่หนึ่ง เราจึงสามารถใช้<br />

คำสั่ง getLastKnownLocation() เพื่อแสดงข้อมูลของตำแหน่งล่าสุดที่ระบบเคยแจ้งไว้ได้ ซึ่งค่าที่<br />

แสดงนั้นจะอยู่ในรูปแบบของละติจูดและลองติจูด รวมทั้งค่าของ UTC (Coordinated Universal<br />

Time), ความสูง, ความเร็ว และทิศทางที่กำลังมุ่งไปด้วย (เรียกใช้ค่าเหล่านี้ด้วยคำสั่ง getAltitude(),<br />

getSpeed() และ getBearing()) และยังใช้คำสั่ง getExtras() เพื่อแสดงข้อมูลของ<br />

ดาวเทียมที่รับสัญญาณในขณะนั้นได้ด้วย ในชุดคำสั่งที่ 10.3 จะแสดงให้เห็นถึงการระบุตำแหน่งบน<br />

จอภาพ<br />

ชุดคำสั่งที่ 10.3 src/com/cookbook/lastlocation/MyLocation.java<br />

ขั้นตอนพื้นฐานของการระบุตำาแหน่ง<br />

package com.cookbook.lastlocation;<br />

import android.app.Activity;<br />

import android.content.Context;<br />

import android.location.Criteria;<br />

import android.location.Location;<br />

import android.location.LocationManager;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

public class MyLocation extends Activity {<br />

LocationManager mLocationManager;<br />

TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

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


254 บทที่ 10 การระบุตำาแหน่ง<br />

mLocationManager = (LocationManager)<br />

getSystemService(Context.LOCATION_SERVICE);<br />

Criteria criteria = new Criteria();<br />

criteria.setAccuracy(Criteria.ACCURACY_FINE);<br />

criteria.setPowerRequirement(Criteria.POWER_LOW);<br />

String locationprovider =<br />

mLocationManager.getBestProvider(criteria,true);<br />

Location mLocation =<br />

mLocationManager.getLastKnownLocation(locationprovider);<br />

}<br />

}<br />

tv.setText("Last location lat:" + mLocation.getLatitude()<br />

+ " long:" + mLocation.getLongitude());<br />

กรรมวิธี: การอัพเดตข้อมูลเมื่อมีการเปลี่ยนแปลงตำแหน่ง<br />

อินเตอร์เฟซ LocationListener จะใช้ในการรับข้อมูล เมื่อตำแหน่งปัจจุบันมีการ<br />

เปลี่ยนแปลง และเมธอด requestLocationUpdates() จะต้องถูกเรียกใช้หลังจากที่ Location<br />

Provider เริ่มทำงาน เพื่อจะได้ตรวจสอบว่าตำแหน่งปัจจุบันมีการเปลี่ยนแปลงหรือไม่ ซึ่งการตรวจ<br />

สอบค่าการเปลี่ยนแปลงนี้จะขึ้นอยู่กับค่าพารามิเตอร์ดังนี้<br />

m provider – ค่าของ Location Provider ที่แอพกำลังใช้งานอยู่<br />

m minTime – ค่าเวลาที่ต่ำที่สุดระหว่างช่วงของการอัพเดตข้อมูล ซึ่งมีหน่วยเป็นมิลลิวินาที<br />

(ระบบปฏิบัติการแอนดรอยด์อาจเพิ่มค่าเวลานี้เพื่อประหยัดพลังงาน)<br />

m minDistance – ระยะทางที่สั้นที่สุดที่มีการเปลี่ยนแปลง ก่อนที่ระบบจะอัพเดตข้อมูล<br />

ซึ่งมีหน่วยเป็นเมตร<br />

m listener – ช่วงเวลาที่ Location Listener ได้รับการอัพเดตข้อมูล<br />

คำสั่ง onLocationChanged() สามารถโอเวอร์ไรด์เพื่อกำหนดแอ็กชั่นที่ต้องการทำหลังจากที่มี<br />

การตรวจพบว่าตำแหน่งปัจจุบันมีการเปลี่ยนแปลง ชุดคำสั่งที่ 10.4 จะแสดงวิธีการกำหนดเงื่อนไขการ<br />

อัพเดตข้อมูลเมื่อกำหนดช่วงเวลาจำนวน 5 วินาที และมีการเปลี่ยนแปลงตำแหน่งมากกว่า 2 เมตร<br />

ซึ่งเวลาใช้งานจริงเราอาจเพิ่มช่วงเวลาให้นานขึ้นเพื่อประหยัดพลังงานและลดปริมาณการใช้หน่วย<br />

ประมวลผลได้ด้วยคำสั่ง onLocationChanged()<br />

ชุดคำสั่งที่ 10.4 src/com/cookbook/update_location/MyLocation.java<br />

package com.cookbook.update_location;<br />

import android.app.Activity;<br />

import android.content.Context;<br />

import android.location.Criteria;<br />

import android.location.Location;<br />

import android.location.LocationListener;


ขั้นตอนพื้นฐานของการระบุตำาแหน่ง<br />

255<br />

import android.location.LocationManager;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

public class MyLocation extends Activity implements LocationListener {<br />

LocationManager mLocationManager;<br />

TextView tv;<br />

Location mLocation;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.tv1);<br />

mLocationManager = (LocationManager)<br />

getSystemService(Context.LOCATION_SERVICE);<br />

Criteria criteria = new Criteria();<br />

criteria.setAccuracy(Criteria.ACCURACY_FINE);<br />

criteria.setPowerRequirement(Criteria.POWER_LOW);<br />

String locationprovider =<br />

mLocationManager.getBestProvider(criteria,true);<br />

}<br />

mLocation =<br />

mLocationManager.getLastKnownLocation(locationprovider);<br />

mLocationManager.requestLocationUpdates(<br />

locationprovider, 5000, 2.0, this);<br />

@Override<br />

public void onLocationChanged(Location location) {<br />

mLocation = location;<br />

showupdate();<br />

}<br />

// these methods are required<br />

public void onProviderDisabled(String arg0) {}<br />

public void onProviderEnabled(String provider) {}<br />

public void onStatusChanged(String a, int b, Bundle c) {}<br />

}<br />

public void showupdate(){<br />

tv.setText("Last location lat:"+mLocation.getLatitude()<br />

+ " long:" + mLocation.getLongitude());<br />

}


256 บทที่ 10 การระบุตำาแหน่ง<br />

จากชุดคำสั่งข้างต้นจะเห็นว่า การเรียกใช้งาน LocationListener ในระดับแอคทิวิตี้ ทำให้เรา<br />

สามารถประกาศอินสแตนซ์โดยใช้คลาสย่อยได้ ซึ่งทำได้โดยใช้ชุดคำสั่งด้านล่างนี้เพื่อเพิ่มขั้นตอนการ<br />

อัพเดตข้อมูลจากตำแหน่งปัจจุบัน<br />

mLocationManager.requestLocationUpdates(<br />

locationprovider, 5000, 2.0, myLocL);<br />

}<br />

private final LocationListener myLocL = new LocationListener(){<br />

@Override<br />

public void onLocationChanged(Location location){<br />

mLocation = location;<br />

showupdate();<br />

}<br />

};<br />

// these methods are required<br />

public void onProviderDisabled(String arg0) {}<br />

public void onProviderEnabled(String provider) {}<br />

public void onStatusChanged(String a, int b, Bundle c) {}<br />

กรรมวิธี: Listing All Enabled Providers<br />

ในหัวข้อนี้เราจะแสดงรายการของ Location Provider ที่สามารถใช้งานได้ในอุปกรณ์<br />

แอนดรอยด์ ผลลัพธ์การทำงานจะแสดงไว้ในรูปที่ 10.1 ซึ่งผลลัพธ์อาจเปลี่ยนแปลงไปตามอุปกรณ์ที่<br />

ใช้งานอยู่ โดยแอคทิวิตี้หลักของการทำงานนี้ จะแสดงไว้ในชุดคำสั่งที่ 10.5 โดยเราจะใช้คำสั่ง<br />

getProviders(true) เพื่อแสดงรายการของ Location Provider<br />

ชุดคำสั่งที่ 10.5 src/com/cookbook/show_providers/MyLocation.java<br />

package com.cookbook.show_providers;<br />

import java.util.List;<br />

import android.app.Activity;<br />

import android.content.Context;<br />

import android.location.Criteria;<br />

import android.location.Location;<br />

import android.location.LocationListener;<br />

import android.location.LocationManager;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

public class MyLocation extends Activity {<br />

LocationManager mLocationManager;<br />

TextView tv;


Location mLocation;<br />

ขั้นตอนพื้นฐานของการระบุตำาแหน่ง<br />

257<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.tv1);<br />

mLocationManager = (LocationManager)<br />

getSystemService(Context.LOCATION_SERVICE);<br />

Criteria criteria = new Criteria();<br />

criteria.setAccuracy(Criteria.ACCURACY_FINE);<br />

criteria.setPowerRequirement(Criteria.POWER_LOW);<br />

String locationprovider =<br />

mLocationManager.getBestProvider(criteria,true);<br />

List providers = mLocationManager.getProviders(true);<br />

StringBuilder mSB = new StringBuilder("Providers:\n");<br />

for(int i = 0; i


258 บทที่ 10 การระบุตำาแหน่ง<br />

รูปที่ 10.1 ตัวอย่างการใช้ Location Provider เพื่อแสดงตำาแหน่งล่าสุด<br />

ซึ่งใช้งานบนอุปกรณ์แอนดรอยด์โดยตรง<br />

กรรมวิธี: การแปลงข้อมูลพิกัดที่อยู่เป็นข้อมูลที่อยู่<br />

คลาส Geocoder จะมีเมธอดที่ใช้แปลงข้อมูลจากข้อมูลที่อยู่ไปเป็นข้อมูลแบบลองติจูด ละติจูด<br />

(Geocoding) และแปลงข้อมูลแบบลองติจูด ละติจูด กลับไปเป็นข้อมูลที่อยู่ (Reverse Geocoding)<br />

สำหรับการแปลงข้อมูลในแบบหลังนี้จะทำให้ได้ข้อมูลบางส่วน เช่น ชื่อเมือง หรือรหัสไปรษณีย์<br />

ซึ่งรายละเอียดของข้อมูลจะขึ้นอยู่กับ Location Provider<br />

ในงานของเรานี้จะใช้ Reverse Geocoding เพื่อหาที่อยู่จากตำแหน่งปัจจุบันของอุปกรณ์<br />

และแสดงผลลัพธ์บนจอภาพดังรูปที่ 10.2 ซึ่งอินสแตนซ์ของ Geocoder จะเริ่มทำงานด้วยค่าภาษา<br />

ท้องถิ่นในกรณีที่ค่านั้นไม่ตรงกับค่าของระบบ ในที่นี้เราจะกำหนดค่าเป็น Locale.ENGLISH แล้ว<br />

เมธอด getFromLocation() ก็จะแสดงรายการของที่อยู่ที่ใกล้เคียงกับพิกัดในขณะนั้น และค่าสูงสุด<br />

ของจำนวนรายการที่จะนำมาแสดงนั้น จะถูกกำหนดให้มีค่าเป็น 1 เพื่อให้ได้ที่อยู่ที่ใกล้เคียงที่สุดเพียง<br />

อันเดียว<br />

คำสั่ง Geocoder จะส่งค่ากลับเป็นรายการของออบเจ็กต์ android.location.Address<br />

การแปลงค่าที่อยู่นี้จะขึ้นอยู่กับเซอร์วิสที่ใช้งาน ซึ่งไม่ได้รวมไว้ในเฟรมเวิร์คของแอนดรอยด์ Google<br />

Map มีการเปิดใช้เซอร์วิส Geocoder ด้วย ซึ่งเราสามารถนำเอาเซอร์วิสนี้มาใช้งานในแอพของเราได้<br />

สำหรับในชุดคำสั่งที่ 10.6 จะแสดงขั้นตอนการทำงานของแอคทิวิตี้หลัก


ขั้นตอนพื้นฐานของการระบุตำาแหน่ง<br />

259<br />

รูปที่ 10.2 ตัวอย่างของการใช้งาน Reverse Geocoding<br />

ซึ่งจะแปลงข้อมูลลองติจูด ละติจูด ไปเป็นข้อมูลที่อยู่<br />

ชุดคำสั่งที่ 10.6 src/com/cookbook/rev_geocoding/MyLocation.java<br />

package com.cookbook.rev_geocoding;<br />

import java.io.IOException;<br />

import java.util.List;<br />

import java.util.Locale;<br />

import android.app.Activity;<br />

import android.content.Context;<br />

import android.location.Address;<br />

import android.location.Criteria;<br />

import android.location.Geocoder;<br />

import android.location.Location;<br />

import android.location.LocationListener;<br />

import android.location.LocationManager;<br />

import android.os.Bundle;<br />

import android.util.Log;<br />

import android.widget.TextView;<br />

public class MyLocation extends Activity {


260 บทที่ 10 การระบุตำาแหน่ง<br />

LocationManager mLocationManager;<br />

Location mLocation;<br />

TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.tv1);<br />

mLocationManager = (LocationManager)<br />

getSystemService(Context.LOCATION_SERVICE);<br />

Criteria criteria = new Criteria();<br />

criteria.setAccuracy(Criteria.ACCURACY_FINE);<br />

criteria.setPowerRequirement(Criteria.POWER_LOW);<br />

String locationprovider =<br />

mLocationManager.getBestProvider(criteria,true);<br />

mLocation =<br />

mLocationManager.getLastKnownLocation(locationprovider);<br />

List addresses;<br />

try {<br />

Geocoder mGC = new Geocoder(this, Locale.ENGLISH);<br />

addresses = mGC.getFromLocation(mLocation.getLatitude(),<br />

mLocation.getLongitude(), 1);<br />

if(addresses != null) {<br />

Address currentAddr = addresses.get(0);<br />

StringBuilder mSB = new StringBuilder("Address:\n");<br />

for(int i=0; i


กรรมวิธี: การแปลงข้อมูลที่อยู่เป็นข้อมูลพิกัด<br />

ในหัวข้อนี้จะแสดงวิธีการแปลงข้อมูลที่อยู่ไปเป็นข้อมูลลองติจูดและละติจูด หรือที่เรียกว่า<br />

Geocoding ขั้นตอนการทำงานจะคล้ายกับในหัวข้อที่แล้ว เพียงแต่เราจะใช้คำสั่ง getFromLocationName()<br />

แทนการใช้คำสั่ง getFromLocation() ชุดคำสั่งที่ 10.7 จะแสดงให้เห็นขั้นตอนการ<br />

แปลงข้อมูลที่อยู่ ซึ่งเก็บอยู่ในตัวแปร myAddress และแปลงไปเป็นค่าพิกัดเพื่อนำไปแสดงผลบน<br />

จอภาพ ดังรูปที่ 10.3<br />

ชุดคำสั่งที่ 10.7 src/com/cookbook/geocoding/MyLocation.java<br />

ขั้นตอนพื้นฐานของการระบุตำาแหน่ง<br />

261<br />

package com.cookbook.geocoding;<br />

import java.io.IOException;<br />

import java.util.List;<br />

import java.util.Locale;<br />

import android.app.Activity;<br />

import android.content.Context;<br />

import android.location.Address;<br />

import android.location.Criteria;<br />

import android.location.Geocoder;<br />

import android.location.Location;<br />

import android.location.LocationListener;<br />

import android.location.LocationManager;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

public class MyLocation extends Activity {<br />

LocationManager mLocationManager;<br />

Location mLocation;<br />

TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

tv = (TextView) findViewById(R.id.tv1);<br />

mLocationManager = (LocationManager)<br />

getSystemService(Context.LOCATION_SERVICE);<br />

Criteria criteria = new Criteria();<br />

criteria.setAccuracy(Criteria.ACCURACY_FINE);<br />

criteria.setPowerRequirement(Criteria.POWER_LOW);<br />

String locationprovider =<br />

mLocationManager.getBestProvider(criteria,true);


262 บทที่ 10 การระบุตำาแหน่ง<br />

mLocation =<br />

mLocationManager.getLastKnownLocation(locationprovider);<br />

List addresses;<br />

String myAddress="Seattle,WA";<br />

Geocoder gc = new Geocoder(this);<br />

try {<br />

addresses = gc.getFromLocationName(myAddress, 1);<br />

if(addresses != null) {<br />

Address x = addresses.get(0);<br />

StringBuilder mSB = new StringBuilder("Address:\n");<br />

}<br />

}<br />

mSB.append("latitude: ").append(x.getLatitude());<br />

mSB.append("\nlongitude: ").append(x.getLongitude());<br />

tv.setText(mSB.toString());<br />

}<br />

} catch(IOException e) {<br />

tv.setText(e.getMessage());<br />

}<br />

รูปที่ 10.3 ตัวอย่างของการใช้คำาสั่ง Geocoding<br />

เพื่อแปลงข้อมูลที่อยู่ไปเป็นข้อมูลแบบลองติจูด ละติจูด


การใช้ Google Maps<br />

การใช้ Google Maps<br />

เราสามารถนำ Google Map มาใช้กับแอนดรอยด์ได้ 2 วิธีด้วยกัน วิธีแรกเป็นการเรียกใช้<br />

Google Maps ผ่านทางเว็บบราวเซอร์โดยตรง และวิธีที่ 2 เป็นการเขียนชุดคำสั่งเพื่อใช้งาน API<br />

ของ Google Maps ซึ่งคลาส MapView จะเป็นคลาสที่ใช้ในการติดต่อกับ Google Maps API<br />

การใช้คลาส MapView มีขั้นตอนดังนี้<br />

1. ดาวน์โหลดและติดตั้ง Google API<br />

1. ใช้ Android SDK และ Android Virtual Device (AVD) ร่วมกับโปรแกรม<br />

Eclipse เพื่อดาวน์โหลด Google API<br />

2. คลิกขวาที่โปรเจ็กต์ที่ต้องการจะใช้งาน API และเลือก Properties<br />

3. เลือก Android แล้วเลือก Google API เพื่อเปิดการใช้งานในโปรเจ็กต์<br />

263<br />

2. สร้างกุญแจ Maps API เพื่อใช้เซอร์วิสแผนที่ของ Google (http://code.google.com/<br />

android/add-ons/google-apis/mapkey.html)<br />

1. ใช้คำสั่ง keytool เพื่อสร้างใบรับรอง MD5 ให้แก่ alias_name<br />

> keytool -list -alias alias_name -keystore my.keystore<br />

> result:(Certificate fingerprint (MD5):<br />

94:1E:43:49:87:73:BB:E6:A6:88:D7:20:F1:8E:B5)<br />

2. ใช้ใบรับรอง MD5 ที่สร้างไว้มาลงทะเบียนเพื่อใช้เซอร์วิสแผนที่ของ Google ที่<br />

http:/code.google.com/android/maps-api-signup.html<br />

3. นำกุญแจที่ได้จากการลงทะเบียนการใช้งานมาใช้ร่วมกับคลาส MapView<br />

3. เพิ่มคำสั่ง <br />

ลงไปในไฟล์ AndroidManifest.xml เพื่อประกาศว่าแอพดังกล่าวมีการเรียกใช้แพ็คเกจ<br />

com.google.android.maps จาก Google API<br />

4. เพิ่มคำสั่ง android.permission.INTERNET ลงไปในไฟล์ AndroidManifest.xml<br />

เพื่อให้แอพสามารถใช้งานอินเตอร์เน็ตเพื่ออ่านข้อมูลจากเซอร์วิสแผนที่ของ Google ได้<br />

5. เพิ่มคำสั่ง MapView ลงไปในไฟล์เลย์เอาต์<br />

ในชุดคำสั่งที่ 10.8 จะแสดงข้อมูลภายในไฟล์ AndroidManifest.xml ที่มีการกำหนดสิทธิ์การใช้<br />

งานแพ็คเกจดังกล่าว


264 บทที่ 10 การระบุตำาแหน่ง<br />

ชุดคำสั่งที่ 10.8 AndroidManifest.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

ในชุดคำสั่งที่ 10.9 แสดงเลย์เอาต์ที่จะใช้งาน MapView เพื่อแสดงแผนที่จาก Google ซึ่งเรา<br />

สามารถกำหนดให้ผู้ใช้สามารถโต้ตอบการทำงานกับแผนที่ได้ โดยให้ประกาศการใช้งานอีลีเมนต์<br />

clickable ปกติแล้วค่าของอีลีเมนต์นี้จะมีค่าเป็น false<br />

ชุดคำสั่งที่ 10.9 res/layout/main.xml<br />

<br />

<br />

<br />


android:apiKey="0ZDUMMY13442HjX491CODE44MSsJzfDVlIQ"<br />

/><br />

<br />

การใช้ Google Maps<br />

265<br />

ไฟล์ที่เราได้เตรียมไว้นั้นจะถูกนำมาใช้งานในหัวข้อถัดไป<br />

กรรมวิธี: การนำ Google Maps มาใช้ในแอพที่พัฒนาขึ้นเอง<br />

การแสดงแผนที่ของ Google ในแอพนั้น เราต้องเรียกใช้คำสั่ง MapActivity โดยแสดงไว้ใน<br />

ชุดคำสั่งที่ 10.10 และต้องกำหนดค่า ID ของเลย์เอาต์ที่จะแสดงแผนที่ด้วย ในที่นี้เราจะใช้ค่า map1<br />

และใช้เมธอด isRouteDisplayed() เพื่อแสดงผล ซึ่งผลลัพธ์ของชุดคำสั่งจะแสดงไว้ในรูปที่ 10.4<br />

ชุดคำสั่งที่ 10.10 src/com/cookbook/using_gmaps/MyLocation.java<br />

package com.cookbook.using_gmaps;<br />

import android.content.Context;<br />

import android.location.Criteria;<br />

import android.location.Location;<br />

import android.location.LocationManager;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

import com.google.android.maps.MapActivity;<br />

import com.google.android.maps.MapView;<br />

public class MyLocation extends MapActivity {<br />

LocationManager mLocationManager;<br />

Location mLocation;<br />

TextView tv;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

MapView mapView = (MapView) findViewById(R.id.map1);<br />

tv = (TextView) findViewById(R.id.tv1);<br />

mLocationManager = (LocationManager)<br />

getSystemService(Context.LOCATION_SERVICE);<br />

Criteria criteria = new Criteria();<br />

criteria.setAccuracy(Criteria.ACCURACY_FINE);<br />

criteria.setPowerRequirement(Criteria.POWER_LOW);<br />

String locationprovider =<br />

mLocationManager.getBestProvider(criteria,true);


266 บทที่ 10 การระบุตำาแหน่ง<br />

mLocation =<br />

mLocationManager.getLastKnownLocation(locationprovider);<br />

}<br />

tv.setText("Last location lat:" + mLocation.getLatitude()<br />

+ " long:" + mLocation.getLongitude());<br />

}<br />

@Override<br />

protected boolean isRouteDisplayed() {<br />

// this method is required<br />

return false;<br />

}<br />

รูปที่ 10.4 ตัวอย่างของการใช้งานแผนที่ของ Google<br />

ในแอพที่พัฒนาขึ้นเอง


การใช้ Google Maps<br />

267<br />

กรรมวิธี: การเพิ่มจุดลงบนแผนที่<br />

คลาส ItemizedOverlay ใช้เพื่อเพิ่มจุดต่างๆ ลงไปบนแผนที่ ซึ่งจะจัดการจุดต่างๆ เหล่านี้<br />

ด้วยอีลีเมนต์ OverlayItem ในหัวข้อนี้เราจะสร้างคลาส ItemizedOverlay และทำการโอเวอร์ไรด์<br />

เมธอดดังนี้<br />

m addOverlay() – ใช้เพื่อเพิ่ม OverlayItem แก่ ArrayList ซึ่งจะเรียกใช้คำสั่ง<br />

populate() ที่ใช้ในการอ่านค่าไอเทมเพื่อใช้วาดบนแผนที่<br />

m createItem() – จะถูกเรียกใช้โดยคำสั่ง populate() เพื่อสร้างจุดจาก OverlayItem<br />

m size() – จะส่งค่ากลับเป็นจำนวนของอีลีเมนต์ OverlayItem ใน ArrayList<br />

m onTap() – จะถูกเรียกใช้เมื่อมีการคลิกที่จุด<br />

ชุดคำสั่งที่ 10.10 จะแสดงการสร้างคลาส ItemizedOverlay ซึ่งจะได้ผลลัพธ์ดังรูปที่ 10.5<br />

ชุดคำสั่งที่ 10.11 src/com/cookbook/adding_markers/MyMarkerLayer.java<br />

package com.cookbook.adding_markers;<br />

import java.util.ArrayList;<br />

import android.app.AlertDialog;<br />

import android.content.DialogInterface;<br />

import android.graphics.drawable.Drawable;<br />

import com.google.android.maps.ItemizedOverlay;<br />

import com.google.android.maps.OverlayItem;<br />

public class MyMarkerLayer extends ItemizedOverlay {<br />

private ArrayList mOverlays =<br />

new ArrayList();<br />

public MyMarkerLayer(Drawable defaultMarker) {<br />

super(boundCenterBottom(defaultMarker));<br />

populate();<br />

}<br />

public void addOverlayItem(OverlayItem overlay) {<br />

mOverlays.add(overlay);<br />

populate();<br />

}<br />

@Override<br />

protected OverlayItem createItem(int i) {<br />

return mOverlays.get(i);<br />

}<br />

@Override<br />

public int size() {<br />

return mOverlays.size();<br />

}


268 บทที่ 10 การระบุตำาแหน่ง<br />

}<br />

@Override<br />

protected boolean onTap(int index) {<br />

AlertDialog.Builder dialog =<br />

new AlertDialog.Builder(MyLocation.mContext);<br />

dialog.setTitle(mOverlays.get(index).getTitle());<br />

dialog.setMessage(mOverlays.get(index).getSnippet());<br />

dialog.setPositiveButton("Ok",<br />

new DialogInterface.OnClickListener() {<br />

public void onClick(DialogInterface dialog, int whichButton) {<br />

dialog.cancel();<br />

}<br />

});<br />

dialog.setNegativeButton("Cancel",<br />

new DialogInterface.OnClickListener() {<br />

public void onClick(DialogInterface dialog, int whichButton) {<br />

dialog.cancel();<br />

}<br />

});<br />

dialog.show();<br />

return super.onTap(index);<br />

}<br />

รูปที่ 10.5 แสดงการเพิ่มจุดที่สามารถคลิกได้บนแผนที่


การใช้ Google Maps<br />

ในชุดคำสั่งที่ 10.11 ในส่วนของคำสั่งที่แสดงเป็นตัวหนานั้น มีคำอธิบายประกอบ ดังนี้<br />

269<br />

m mOverlays ถูกสร้างขึ้นเพื่อจัดเก็บข้อมูลของไอเทมทั้งหมด และส่งผ่านค่าดังกล่าวไปยัง<br />

Overlay<br />

m เราจะต้องกำหนดจุดเชื่อมต่อของแต่ละไอเทมที่จะนำไปวางบนแผนที่ก่อนที่จะวาดจุดจาก<br />

ไอเทมดังกล่าว การอ้างอิงถึงตำแหน่งตรงกลางด้านล่างของแผนที่นั้น เราจะใช้คำสั่ง<br />

boundCenterBottom ใส่ไว้ในคลาสหลักของแอพ<br />

m เมธอดที่ต้องโอเวอร์ไรด์คือ addOverlay(), createItem(), size() และ onTap()<br />

ซึ่งคำสั่ง onTap() จะใช้แสดงข้อความไดอะล็อกบ็อกซ์เมื่อมีการคลิกที่จุด<br />

m เมธอด populate() และคำสั่ง addOverlay() ที่เพิ่มในช่วงท้ายของเป็นการแจ้งให้<br />

คลาส MyMarkerLayer เตรียมอีลีเมนต์ OverlayItem ทั้งหมด และวาดจุดแต่ละจุดลง<br />

บนแผนที่<br />

หลังจากนี้เราก็สามารถนำ ItemizedOverlay ไปเพิ่มใน MapActivity ที่สร้างไว้ในตัวอย่าง<br />

ก่อนหน้านี้ได้เลย ซึ่งในชุดคำสั่งที่ 10.11 ในส่วนของคำสั่งที่แสดงเป็นตัวหนานั้น มีคำอธิบายประกอบ<br />

ดังนี้<br />

1. เราจะใช้คำสั่ง getOverlays() ของคลาส MapView ในการตรวจสอบหา Overlay ที่มี<br />

การใช้อยู่แล้วบนแผนที่ โดยที่เราจะเพิ่มเลย์เยอร์ของการวางจุดไว้ในคอนเทนเนอร์นี้<br />

ในส่วนท้ายของฟังก์ชั่น<br />

2. อินสแตนซ์ MyMarkerLayer ใช้เก็บข้อมูลของ OverlayItem<br />

3. การอ่านค่าละติจูดและลองติจูดของข้อมูลที่อยู่นั้น เราจะใช้คลาส GeoPoint ซึ่งค่าที่ได้<br />

จะมีหน่วยเป็นไมโครดีกรี ดังนั้นค่าละติดจูดและลองติจูดที่ได้จะต้องนำไปหารด้วยค่า<br />

1,000,000 ก่อนเสมอ<br />

4. ผู้ใช้งานสามารถใช้ตัวควบคุมแผนที่ในการย่อและขยายวิว ซึ่งทำได้โดยใช้คำสั่ง<br />

setBuiltInZoomControls() เพื่อควบคุมการย่อและขยายวิว<br />

5. กำหนด OverlayItem ชนิดข้อความเพิ่มเติมให้แก่ GeoPoint เพื่อความน่าสนใจของ<br />

แอพ<br />

6. เพิ่มไอเทมลงใน MyMarkerLayer โดยใช้เมธอด addOverlayItem() และเพิ่ม<br />

MyMarkerLayer นี้ลงไปใน Overlay ที่มีอยู่แล้ว ซึ่งใช้คำสั่ง getOverlay() ในการ<br />

ตรวจสอบ<br />

ชุดคำสั่งที่ 10.12 src/com/cookbook/adding_markers/MyLocation.java<br />

package com.cookbook.adding_markers;<br />

import java.io.IOException;<br />

import java.util.List;<br />

import android.content.Context;<br />

import android.graphics.drawable.Drawable;


270 บทที่ 10 การระบุตำาแหน่ง<br />

import android.location.Address;<br />

import android.location.Geocoder;<br />

import android.os.Bundle;<br />

import android.widget.TextView;<br />

import com.google.android.maps.GeoPoint;<br />

import com.google.android.maps.MapActivity;<br />

import com.google.android.maps.MapController;<br />

import com.google.android.maps.MapView;<br />

import com.google.android.maps.Overlay;<br />

public class MyLocation extends MapActivity {<br />

TextView tv;<br />

List mapOverlays;<br />

MyMarkerLayer markerlayer;<br />

private MapController mc;<br />

public static Context mContext;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

mContext = this;<br />

setContentView(R.layout.main);<br />

MapView mapView = (MapView) findViewById(R.id.map1);<br />

tv = (TextView) findViewById(R.id.tv1);<br />

mapOverlays = mapView.getOverlays();<br />

Drawable drawable =<br />

this.getResources().getDrawable(R.drawable.icon);<br />

markerlayer = new MyMarkerLayer(drawable);<br />

List addresses;<br />

String myAddress="1600 Amphitheatre Parkway, Mountain View, CA";<br />

int geolat = 0;<br />

int geolon = 0;<br />

Geocoder gc = new Geocoder(this);<br />

try {<br />

addresses = gc.getFromLocationName(myAddress, 1);<br />

if(addresses != null) {<br />

Address x = addresses.get(0);<br />

geolat = (int)(x.getLatitude()*1E6);<br />

geolon = (int)(x.getLongitude()*1E6);<br />

}<br />

} catch(IOException e) {<br />

tv.setText(e.getMessage());<br />

}<br />

mapView.setBuiltInZoomControls(true);<br />

GeoPoint point = new GeoPoint(geolat,geolon);<br />

mc = mapView.getController();<br />

mc.animateTo(point);


mc.setZoom(3);<br />

การใช้ Google Maps<br />

271<br />

}<br />

OverlayItem overlayitem =<br />

new OverlayItem(point, "Google Campus", "I am at Google");<br />

markerlayer.addOverlayItem(overlayitem);<br />

mapOverlays.add(markerlayer);<br />

}<br />

@Override<br />

protected boolean isRouteDisplayed() { return false; }<br />

กรรมวิธี: การเพิ่มวิวลงบนแผนที่<br />

เราสามารถเพิ่ม View หรือ ViewGroup ลงไปใน MapView ได้ โดยในหัวข้อนี้จะแสดงการเพิ่ม<br />

อีลีเมนต์จำนวน 2 ตัวลงไปในแผนที่ ได้แก่ TextView และ Button เมื่อปุ่มถูกกด ข้อความใน<br />

TextView ก็จะเปลี่ยนแปลง เราจะเพิ่มวิวลงไปใน MapView ด้วยการใช้เมธอด addView() และค่า<br />

ของ LayoutParams ซึ่งตำแหน่งของอีลีเมนต์จะกำหนดเป็นค่า x ,y บนจอภาพ ทั้งนี้ทั้งนั้น<br />

เราสามารถใช้ค่าของ GeoPoint กำหนดให้แก่ LayoutParams ได้เช่นกัน ชุดคำสั่งที่ 10.13 จะแสดง<br />

การทำงานของแอคทิวิตี้หลักที่จะต้องใช้คลาส MyMarkerLayer ที่ได้สร้างไว้แล้วในหัวข้อก่อนหน้านี้<br />

โดยผลลัพธ์ของการใช้ MapView จะแสดงอยู่ในรูปที่ 10.6<br />

ชุดคำสั่งที่ 10.13 src/com/cookbook/mylocation/MyLocation.java<br />

package com.cookbook.mylocation;<br />

import java.io.IOException;<br />

import java.util.List;<br />

import android.content.Context;<br />

import android.content.Intent;<br />

import android.graphics.Color;<br />

import android.graphics.drawable.Drawable;<br />

import android.location.Address;<br />

import android.location.Geocoder;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

import android.widget.TextView;<br />

import com.google.android.maps.GeoPoint;<br />

import com.google.android.maps.MapActivity;<br />

import com.google.android.maps.MapController;<br />

import com.google.android.maps.MapView;<br />

import com.google.android.maps.Overlay;<br />

public class MyLocation extends MapActivity {<br />

TextView tv;


272 บทที่ 10 การระบุตำาแหน่ง<br />

List mapOverlays;<br />

MyMarkerLayer markerlayer;<br />

private MapController mc;<br />

MapView.LayoutParams mScreenLayoutParams;<br />

public static Context mContext;<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

mContext = this;<br />

setContentView(R.layout.main);<br />

MapView mapView = (MapView) findViewById(R.id.map1);<br />

mc = mapView.getController();<br />

tv = (TextView) findViewById(R.id.tv1);<br />

mapOverlays = mapView.getOverlays();<br />

Drawable drawable =<br />

this.getResources().getDrawable(R.drawable.icon);<br />

markerlayer = new MyMarkerLayer(drawable);<br />

List addresses;<br />

String myAddress="1600 Amphitheatre Parkway, Mountain View, CA";<br />

int geolat = 0;<br />

int geolon = 0;<br />

Geocoder gc = new Geocoder(this);<br />

try {<br />

addresses = gc.getFromLocationName(myAddress, 1);<br />

if(addresses != null) {<br />

Address x = addresses.get(0);<br />

StringBuilder mSB = new StringBuilder("Address:\n");<br />

geolat =(int)(x.getLatitude()*1E6);<br />

geolon = (int)(x.getLongitude()*1E6);<br />

mSB.append("latitude: ").append(geolat).append("\n");<br />

mSB.append("longitude: ").append(geolon);<br />

tv.setText(mSB.toString());<br />

}<br />

} catch(IOException e) {<br />

tv.setText(e.getMessage());<br />

}<br />

int x = 50;<br />

int y = 50;<br />

mScreenLayoutParams =<br />

new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT,<br />

MapView.LayoutParams.WRAP_CONTENT,<br />

x,y,MapView.LayoutParams.LEFT);<br />

final TextView tv = new TextView(this);<br />

tv.setText("Adding View to Google Map");<br />

tv.setTextColor(Color.BLUE);<br />

tv.setTextSize(20);


mapView.addView(tv, mScreenLayoutParams);<br />

การใช้ Google Maps<br />

273<br />

x = 250;<br />

y = 250;<br />

mScreenLayoutParams =<br />

new MapView.LayoutParams(MapView.LayoutParams.WRAP_CONTENT,<br />

MapView.LayoutParams.WRAP_CONTENT,<br />

x,y,<br />

MapView.LayoutParams.BOTTOM_CENTER);<br />

Button clickMe = new Button(this);<br />

clickMe.setText("Click Me");<br />

clickMe.setOnClickListener(new OnClickListener() {<br />

public void onClick(View v) {<br />

tv.setTextColor(Color.RED);<br />

tv.setText("Let’s play");<br />

}<br />

});<br />

}<br />

mapView.addView(clickMe, mScreenLayoutParams);<br />

}<br />

@Override<br />

protected boolean isRouteDisplayed() { return false; }


274 บทที่ 10 การระบุตำาแหน่ง<br />

รูปที่ 10.16 ตัวอย่างการเพิ่มวิวลงไปในแผนที่<br />

กรรมวิธี: การกำหนดตำแหน่งปัจจุบันของอุปกรณ์ลงบนแผนที่<br />

ความสะดวกของการใช้งาน MyLocationOverlay ก็คือ มันจะวาดแผนที่ใหม่ที่อ้างอิงกับ<br />

ตำแหน่งปัจจุบันให้อัตโนมัติ โดยแสดงตำแหน่งเป็นจุดสีน้ำเงิน ซึ่งจะแสดงข้อมูลทิศทางที่กำลัง<br />

เคลื่อนที่ รวมทั้งค่าความถูกต้องของพิกัด มีเมธอดที่ใช้งานบ่อยๆ ดังนี้<br />

m enableCompass() – ใช้เพื่อเปิดการแสดงเข็มทิศบนแผนที่<br />

m enableMyLocation() – แสดงตำแหน่งปัจจุบันโดยใช้สัญลักษณ์เป็นจุดสีน้ำเงิน<br />

และล้อมรอบด้วยวงกลมเพื่อแสดงค่าความถูกต้องของพิกัด<br />

m getMyLocation() – จะส่งค่าพิกัดของตำแหน่งปัจจุบัน ซึ่งมีชนิดของข้อมูลเป็น<br />

GeoPoint<br />

m getOrientation() – จะส่งค่าทิศทางที่กำลังเคลื่อนที่ไป<br />

เมธอดเหล่านี้จะช่วยให้ผู้พัฒนาสามารถใช้ประโยชน์จากแผนที่ได้มากขึ้น


การใช้ Google Maps<br />

275<br />

กรรมวิธี: การกำหนดข้อความแจ้งเตือนเมื่อออกจากพื้นที่ที่กำหนดไว้<br />

คลาส LocationManager มีเมธอดที่ใช้ในการทำงานประเภทข้อความแจ้งเตือนเมื่อออกจาก<br />

พื้นที่ที่กำหนดไว้ (Proximity) ซึ่งจะแสดงข้อความเตือนเมื่อมีการเคลื่อนย้ายอุปกรณ์ออกนอกพื้นที่ที่<br />

กำหนดไว้ โดยเราจะกำหนดขอบเขตของพื้นที่ดังกล่าวด้วยค่าละติจูดและลองติจูด และค่าของระยะ<br />

ทางซึ่งมีหน่วยเป็นเมตร ส่วนข้อความเตือนจะกำหนดไว้ใน PendingIntent ซึ่งจะแสดงเมื่อผู้ใช้งาน<br />

ออกนอกพื้นที่ที่กำหนด นอกจากนี้เรายังสามารถกำหนดระยะเวลาที่จะแสดงข้อความเตือนได้ด้วย<br />

ชุดคำสั่งที่ 10.14 จะแสดงขั้นตอนการทำงานตามรายละเอียดที่กล่าวมาแล้ว<br />

ชุดคำสั่งที่ 10.14 ตัวอย่างของการกำหนดข้อความแจ้งเตือนเมื่อออกจากพื้นที่ที่กำหนดไว้<br />

double mlatitude=35.41;<br />

double mlongitude=139.46;<br />

float mRadius=500f; // in meters<br />

long expiration=-1; //-1 never expires or use milliseconds<br />

Intent mIntent = new Intent("You entered the defined area");<br />

PendingIntent mFireIntent<br />

= PendingIntent.getBroadCast(this, -1, mIntent, 0);<br />

mLocationManager.addProximityAlert(mlatitude, mlongitude,<br />

mRadius, expiration, mFireIntent);


276 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์


277<br />

บทที่ 11<br />

เทคนิคขั้นสูงสำหรับพัฒนาแอพบนแอนดรอยด์<br />

ในบทนี้จะกล่าวถึงการใช้งานเทคนิคขั้นสูงต่างๆ ที่เราสามารถนำมาใช้ในแอพเพื่อเพิ่ม<br />

ประสิทธิภาพและความน่าสนใจโดยไล่ตั้งแต่การสร้างวิวขึ้นมาใช้เอง, การใช้ NDK (Native Development<br />

Kit) เพื่อลดภาระในการประมวลผลและเพิ่มความเร็วในการทำงานในกรณีที่มีการทำงานของชุด<br />

คำสั่งที่ซับซ้อน, ระบบความปลอดภันต่างๆ ที่มีในระบบปฏิบัติการแอนดรอยด์ รวมทั้งการสำรองข้อมูล<br />

และนำข้อมูลไปเก็บยังระบบคลาวด์ ซึ่งระบบนี้จะใช้ได้กับแอนดรอยด์เวอร์ชั่น 2.2 และยังกล่าวถึง<br />

เทคนิคที่ใช้ในการสร้างส่วนการติดต่อกับผู้ใช้งาน (UI) ด้วย<br />

การสร้างวิวขึ้นเอง (Android Custom View)<br />

จากที่ได้กล่าวถึงในบทที่ 4 ระบบปฏิบัติการแอนดรอยด์จะมีวิวอยู่ 2 ชนิด คือ View และ<br />

ViewGroup การสร้างวิวขึ้นเองนั้น เราสามารถสร้างโดยเริ่มต้นใหม่ทั้งหมดหรือจะใช้วิวอื่นๆ ที่มีอยู่<br />

แล้วมาปรับปรุงเพิ่มเติมได้ วิดเจ็ตมาตรฐานของเฟรมเวิร์คแอนดรอยด์จะใช้งานภายใต้คลาส View<br />

หรือ ViewGroup ซึ่งออบเจ็กต์ที่เราสามารถนำมาสร้าง View หรือ ViewGroup ขึ้นเองนั้น มีดังนี้<br />

m Views - Button, EditText, TextView, ImageView และอื่นๆ<br />

m ViewGroups - LinearLayout, ListView, RelativeLayout, RadioGroup<br />

และอื่นๆ<br />

กรรมวิธี: การปรับแต่งปุ่มกด<br />

ในหัวข้อนี้เราจะมาปรับแต่งปุ่มกดกันโดยใช้คลาสชื่อ myButton ซึ่งสร้างมาจากคลาส Button<br />

สำหรับคลาส myButton จะมีคุณสมบัติเช่นเดียวกับคลาส Button การปรับแต่งปุ่มกดนั้นเราจะใช้<br />

เมธอด onMeasure() และ onDraw()<br />

เมธอด onMeasure() จะใช้กำหนดขนาดที่ต้องการ ซึ่งประกอบไปด้วย 2 ค่า คือ ค่าความ<br />

กว้าง และค่าความสูง ในการปรับแต่งปุ่มกดนั้นเราจะคำนวณค่าความกว้างและสูงโดยพิจารณาจาก<br />

ข้อมูลที่แสดงอยู่บนปุ่มกด ซึ่งสามารถกำหนดค่าได้โดยใช้คำสั่ง setMeasuredDimension() และถ้า<br />

เกิดข้อผิดพลาด measure() ก็จะสร้างอีเวนต์ illegalStateException<br />

เราจะใช้เมธอด onDraw() มาปรับแต่งการวาดบนปุ่มกด ซึ่งภาพที่แสดงนั้นใช้วิธีการเรนเดอร์<br />

ลงบนวิว และถ้าพื้นหลังของวิวมีการกำหนดให้มีค่าเป็น Drawable เราก็จะต้องวาดวิวดังกล่าวก่อนที่<br />

จะใช้คำสั่ง onDraw() เพื่อวาดภาพให้แก่ปุ่มกด


278 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

ภายในคลาส myButton จะมีเมธอดจำนวน 8 ตัว ที่สามารถเรียกใช้งานได้ ดังนี้<br />

m setText() – ใช้กำหนดข้อความที่จะแสดงบนปุ่มกด<br />

m setTextSize() – ใช้กำหนดขนาดของข้อความ<br />

m setTextColor() –ใช้กำหนดสีของข้อความ<br />

m measureWidth() – ใช้กำหนดค่าความกว้างของปุ่มกด<br />

m measureHeight() – ใช้กำหนดค่าความสูงของปุ่มกด<br />

m drawArcs() – ใช้วาดเส้นโค้ง<br />

m onDraw() – ใช้วาดภาพกราฟิกลงบนปุ่ม<br />

m onMeasure() – ใช้กำหนดขนาดและขอบเขตการทำงานของปุ่มกด<br />

เมธอด setText(), setTextSize() และ setTextColor() จะเปลี่ยนแปลงค่าแอททริบิวต์<br />

ของข้อความ ทุกๆ ครั้งที่ข้อความมีการเปลี่ยนแปลง เมธอด invalidate() จะต้องถูกเรียกใช้งาน<br />

เพื่อแสดงผลข้อความใหม่พร้อมๆ กับค่าที่กำหนดไว้ เมธอด setText() และ setTextSize()<br />

จะเรียกใช้คำสั่ง requestLayout() แต่จะไม่เรียกใช้คำสั่งนี้ในเมธอด setTextColor() เพราะว่า<br />

การเปลี่ยนแปลงสีของข้อความนั้นไม่ได้ส่งผลถึงขนาดและขอบเขตของปุ่มกด<br />

ภายในเมธอด onMeasure() คำสั่ง setMeasureDimension() จะถูกเรียกใช้พร้อมกับคำสั่ง<br />

measureWidth() และ measureHeight() ซึ่งค่าเหล่านี้เป็นค่าที่จะต้องกำหนดในกรณีที่จะสร้างวิว<br />

ขึ้นเอง<br />

คำสั่ง measureWidth() และ measureHeight() จะถูกเรียกใช้พร้อมกับค่าความสูงของวิว<br />

หลัก พร้อมทั้งส่งค่าความสูงและความกว้างกลับโดยใช้หน่วยตามค่าที่กำหนดไว้ ถ้าเรากำหนดโหมด<br />

ของการวัดเป็น EXACTLY เมธอดก็จะส่งค่ากลับโดยการใช้ค่าที่ได้จากวิวหลัก แต่ถ้ากำหนดโหมดของ<br />

การวัดเป็น AT_MOST เมธอดก็จะส่งค่ากลับเป็นค่าที่ต่ำสุดของขนาดคอนเทนต์ อย่างในตัวอย่างของ<br />

เรานี้ ขนาดของคอนเทนต์จะอ้างอิงตามขนาดของข้อความ<br />

คำสั่ง drawArcs() เป็นคำสั่งที่ใช้วาดเส้นโค้งลงบนปุ่มกด ซึ่งเราสามารถทำให้เป็นภาพ<br />

เคลื่อนไหวได้ ทุกๆ ครั้งที่วาดเส้นโค้ง ความยาวของเส้นก็จะเพิ่มขึ้นเล็กน้อยและมีการไล่โทนสีเพื่อให้<br />

แสดงภาพเคลื่อนไหวที่สมบูรณ์<br />

คลาสที่ใช้สร้างปุ่มขึ้นเองนั้นจะแสดงไว้ในชุดคำสั่งที่ 11.1 โดยเมธอด myButton() 2 ตัวที่<br />

แสดงไว้นั้นจะเป็นไปตามค่าอาร์กิวเมนต์ที่กำหนดไว้ โดยในแต่ละตัวจะมีลาเบลและแอททริบิวท์ของ<br />

ตัวเอง ไลบรารี android.graphic.* จะมีการทำงานที่คล้ายกับที่ใช้ในภาษาจาวา เช่น คำสั่ง<br />

Matrix และ Paint


ชุดคำสั่งที่ 11.1 src/com/cookbook/advance/MyButton.java<br />

การสร้างวิวขึ้นเอง (Android Custom View)<br />

279<br />

package com.cookbook.advance.customComponent;<br />

import android.content.Context;<br />

import android.graphics.Canvas;<br />

import android.graphics.Color;<br />

import android.graphics.Matrix;<br />

import android.graphics.Paint;<br />

import android.graphics.RectF;<br />

import android.graphics.Shader;<br />

import android.graphics.SweepGradient;<br />

import android.util.AttributeSet;<br />

import android.util.Log;<br />

import android.widget.Button;<br />

public class MyButton extends Button {<br />

private Paint mTextPaint, mPaint;<br />

private String mText;<br />

private int mAscent;<br />

private Shader mShader;<br />

private Matrix mMatrix = new Matrix();<br />

private float mStart;<br />

private float mSweep;<br />

private float mRotate;<br />

private static final float SWEEP_INC = 2;<br />

private static final float START_INC = 15;<br />

public MyButton(Context context) {<br />

super(context);<br />

initLabelView();<br />

}<br />

public MyButton(Context context, AttributeSet attrs) {<br />

super(context, attrs);<br />

initLabelView();<br />

}<br />

private final void initLabelView() {<br />

mTextPaint = new Paint();<br />

mTextPaint.setAntiAlias(true);<br />

mTextPaint.setTextSize(16);<br />

mTextPaint.setColor(0xFF000000);<br />

setPadding(15, 15, 15, 15);<br />

mPaint = new Paint();<br />

mPaint.setAntiAlias(true);<br />

mPaint.setStrokeWidth(4);<br />

mPaint.setAntiAlias(true);


280 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

}<br />

mPaint.setStyle(Paint.Style.STROKE);<br />

mShader = new SweepGradient(this.getMeasuredWidth()/2,<br />

this.getMeasuredHeight()/2,<br />

new int[] { Color.GREEN,<br />

Color.RED,<br />

Color.CYAN,Color.DKGRAY },<br />

null);<br />

mPaint.setShader(mShader);<br />

public void setText(String text) {<br />

mText = text;<br />

requestLayout();<br />

invalidate();<br />

}<br />

public void setTextSize(int size) {<br />

mTextPaint.setTextSize(size);<br />

requestLayout();<br />

invalidate();<br />

}<br />

public void setTextColor(int color) {<br />

mTextPaint.setColor(color);<br />

invalidate();<br />

}<br />

@Override<br />

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){<br />

setMeasuredDimension(measureWidth(widthMeasureSpec),<br />

measureHeight(heightMeasureSpec));<br />

}<br />

private int measureWidth(int measureSpec) {<br />

int result = 0;<br />

int specMode = MeasureSpec.getMode(measureSpec);<br />

int specSize = MeasureSpec.getSize(measureSpec);<br />

if (specMode == MeasureSpec.EXACTLY) {<br />

// We were told how big to be<br />

result = specSize;<br />

} else {<br />

// Measure the text<br />

result = (int) mTextPaint.measureText(mText)<br />

+ getPaddingLeft()<br />

+ getPaddingRight();<br />

if (specMode == MeasureSpec.AT_MOST) {<br />

result = Math.min(result, specSize);<br />

}


}<br />

การสร้างวิวขึ้นเอง (Android Custom View)<br />

281<br />

}<br />

return result;<br />

private int measureHeight(int measureSpec) {<br />

int result = 0;<br />

int specMode = MeasureSpec.getMode(measureSpec);<br />

int specSize = MeasureSpec.getSize(measureSpec);<br />

}<br />

mAscent = (int) mTextPaint.ascent();<br />

if (specMode == MeasureSpec.EXACTLY) {<br />

// We were told how big to be<br />

result = specSize;<br />

} else {<br />

// Measure the text (beware: ascent is a negative number)<br />

result = (int) (-mAscent + mTextPaint.descent())<br />

+ getPaddingTop() + getPaddingBottom();<br />

if (specMode == MeasureSpec.AT_MOST) {<br />

Log.v("Messure Height", "At most Height:"+specSize);<br />

result = Math.min(result, specSize);<br />

}<br />

}<br />

return result;<br />

private void drawArcs(Canvas canvas, RectF oval, boolean useCenter,<br />

Paint paint) {<br />

canvas.drawArc(oval, mStart, mSweep, useCenter, paint);<br />

}<br />

@Override protected void onDraw(Canvas canvas) {<br />

mMatrix.setRotate(mRotate, this.getMeasuredWidth()/2,<br />

this.getMeasuredHeight()/2);<br />

mShader.setLocalMatrix(mMatrix);<br />

mRotate += 3;<br />

if (mRotate >= 360) {<br />

mRotate = 0;<br />

}<br />

RectF drawRect = new RectF();<br />

drawRect.set(this.getWidth()-mTextPaint.measureText(mText),<br />

(this.getHeight()-mTextPaint.getTextSize())/2,


}<br />

282 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

}<br />

mTextPaint.measureText(mText),<br />

this.getHeight()-(this.getHeight()-mTextPaint.getTextSize())/2);<br />

drawArcs(canvas, drawRect, false, mPaint);<br />

mSweep += SWEEP_INC;<br />

if (mSweep > 360) {<br />

mSweep -= 360;<br />

mStart += START_INC;<br />

if (mStart >= 360) {<br />

mStart -= 360;<br />

}<br />

}<br />

if(mSweep >180){<br />

canvas.drawText(mText, getPaddingLeft(),<br />

getPaddingTop() -mAscent, mTextPaint);<br />

}<br />

invalidate();<br />

ชุดคำสั่งที่ 11.2 จะแสดงเลย์เอาต์ที่มีการใช้ปุ่มที่สร้างขึ้นเอง<br />

ชุดคำสั่งที่ 11.2 res/layout/main.xml<br />

<br />

<br />

<br />

<br />

ในไฟล์เลย์เอาต์ XML จะมี ViewGroup, LinearLayout และ View เพียงอย่างละหนึ่งตัว<br />

ซึ่งสามารถอ้างอิงถึงปุ่มที่สร้างขึ้นด้วยคำสั่ง com.cookbook.advance.customComponent.<br />

myButton และสามารถนำมาใช้งานภายในแอคทิวิตี้ได้ ดังแสดงไว้ในชุดคำสั่งที่ 11.3<br />

ชุดคำสั่งที่ 11.3 src/com/cookbook/advance/ShowMyButton.java<br />

package com.cookbook.advance.customComponent;<br />

import android.app.Activity;<br />

import android.os.Bundle;


การใช้งานคอมโพเน็นต์ของแอนดรอยด์แบบที่พัฒนาด้วยภาษา C, C++<br />

283<br />

public class ShowMyButton extends Activity{<br />

@Override<br />

protected void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

MyButton myb = (MyButton)findViewById(R.id.mybutton1);<br />

myb.setText("Hello Students");<br />

myb.setTextSize(40);<br />

}<br />

}<br />

จากตัวอย่างข้างต้นเป็นการใช้งานปุ่มที่สร้างขึ้นเอง ซึ่งมีการทำงานเหมือนกับการใช้งานปุ่มแบบ<br />

ปกติ ผลลัพธ์ของชุดคำสั่งแสดงอยู่ในรูปที่ 11.1<br />

รูปที่ 11.1 ตัวอย่างของปุ่มที่สร้างขึ้นเอง<br />

การใช้งานคอมโพเน็นต์ของแอนดรอยด์แบบที่พัฒนาด้วย<br />

ภาษา C, C++<br />

ในกรณีที่ชุดคำสั่งในแอพของเรามีการทำงานที่ซับซ้อน ใช้เวลาในการประมวลผลมาก รวมทั้ง<br />

เป็นส่วนสำคัญในการทำงานของแอพ เราจะใช้วิธีย้ายชุดคำสั่งดังกล่าวออกมาทำงานในลักษณะของ<br />

ภาษา C หรือ C++ เพื่อเพิ่มประสิทธิภาพการทำงาน โดยเราจะใช้ Android NDK มาช่วยพัฒนาใน<br />

ส่วนนี้


284 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

Android NDK จะทำงานร่วมกับ Android SDK ซึ่งมีไลบรารีที่สามารถนำมาใช้ในการสร้าง<br />

ไลบรารีแบบ C/C++ ได้ ขั้นตอนการสร้าง Android NDK มีดังนี้<br />

1. ดาวน์โหลด Android NDK จาก http://developer.android.com/sdk/ndk/ ซึ่งมีเอกสาร<br />

ประกอบการใช้งานเผยแพร่ไว้ด้วย<br />

2. สร้างโปรเจ็กต์แอนดรอยด์ แต่จะกำหนดไว้ในไดเร็กทอรี NDK<br />

3. สร้างไดเร็กทอรี jni/ ภายใต้โปรเจ็กต์ที่สร้างไว้ในขั้นตอนที่ 2<br />

4. สร้างชุดคำสั่งภาษา C/C++ ที่ต้องการใช้งานไว้ในไดเร็กทอรี jni/<br />

5. สร้างไฟล์ Android.mk<br />

6. รันโปรแกรมสร้างสคริปต์ (ndk-build ในกรณที่ใช้ NDK-r4) ในไดเร็กทอรีของโปรเจ็กต์<br />

7. อิมพอร์ตไลบรารีที่สร้างขึ้นมาไว้ในโปรเจ็กต์แอนดรอยด์เพื่อเรียกใช้งานฟังก์ชั่น<br />

ถ้าเราใช้โปรแกรม Eclipse ในการพัฒนา ไลบรารีที่เรียกใช้จะถูกรวมไว้ในแอพระหว่างที่ทำ<br />

การบิวด์ (Build) โดยอัตโนมัติ<br />

กรรมวิธี: การสร้าง Native Component<br />

ในหัวข้อนี้เราจะใช้โปรแกรมภาษา C ในการสร้างฟังก์ชั่นเพื่อหาค่าแฟคทอเรียล โดยแอคทิวิตี้<br />

จะเรียกใช้ฟังก์ชั่นภาษา C และแสดงผลลัพธ์ที่ได้บนจอภาพ รายละเอียดการทำงานของชุดคำสั่ง<br />

ภาษา C จะแสดงไว้ในชุดคำสั่งที่ 11.4<br />

ชุดคำสั่งที่ 11.4 jni/cookbook.c<br />

#include <br />

#include <br />

jint factorial(jint n){<br />

if(n == 1){<br />

return 1;<br />

}<br />

return factorial(n-1)*n;<br />

}<br />

jint Java_com_cookbook_advance_ndk_ndk_factorial( JNIEnv* env,<br />

jobject thiz, jint n ) {<br />

return factorial(n);<br />

}


การใช้งานคอมโพเน็นต์ของแอนดรอยด์แบบที่พัฒนาด้วยภาษา C, C++<br />

285<br />

ภายในชุดคำสั่งภาษา C จะมีการกำหนดชนิดของข้อมูลเป็น jint ซึ่งเป็นการประกาศชนิดของ<br />

ข้อมูลที่ใช้ในภาษาจาวา โดยเราจะใช้เพื่อส่งผ่านข้อมูลระหว่างกัน ถ้ามีการส่งค่าจากภาษาจาวาไปยัง<br />

ภาษา C ก็จะมีการเปลี่ยนรูปแบบของข้อมูล ในตารางที่ 11.1 จะแสดงชนิดของข้อมูลเปรียบเทียบกัน<br />

ระหว่างภาษาจาวาและภาษา C<br />

ตารางที่ 11.1 แสดงชนิดของข้อมูลเปรียบเทียบกันระหว่างภาษาจาวาและภาษา C<br />

Java Type ใน C/C++ Native Type รายละเอียด<br />

Jboolean Unsigned char<br />

ข้อมูลขนาด 8 บิต ไม่รวมเครื่องหมาย<br />

Jbyte Signed char<br />

ข้อมูลขนาด 8 บิต รวมเครื่องหมาย<br />

Jchar Unsigned short<br />

ข้อมูลขนาด 16 บิต ไม่รวมเครื่องหมาย<br />

Jshort Short<br />

ข้อมูลขนาด 16 บิต รวมเครื่องหมาย<br />

Jint Long<br />

ข้อมูลขนาด 32 บิต รวมเครื่องหมาย<br />

Jfloat Float<br />

ข้อมูลขนาด 32 บิต<br />

Jlong Long long_int64<br />

ข้อมูลขนาด 64 บิต รวมเครื่องหมาย<br />

jdouble Double<br />

ข้อมูลขนาด 64 บิต<br />

ในตัวอย่างโปรแกรมภาษา C จะมีฟังก์ชั่นอยู่ 2 ตัว ตัวแรกคือฟังก์ชั่น factorial ซึ่งจะใช้ใน<br />

การคำนวณหาค่าแฟคทอเรียล และตัวที่ 2 ฟังก์ชั่น Java Call ซึ่งชื่อของฟังก์ชั่นจะต้องประกาศไว้ใน<br />

รูปแบบ JAVA_CLASSNAME_METHOD เพื่อให้สามารถติดต่อกับแอพอื่นๆ ได้<br />

ในฟังก์ชั่นตัวที่ 2 จะมีการกำหนดค่าพารามิเตอร์จำนวน 3 ตัว คือ JNIEnv และ jobject<br />

ซึ่งเป็นพอยท์เตอร์และจาวาอาร์กิวเมนต์ JNIEnv เป็นจาวาอินเตอร์เฟซที่ใช้ในการส่งผ่านค่า<br />

อาร์กิวเมนต์ไปสู่ฟังก์ชั่นภาษา C โดยจะทำงานบน Java Virtual Machine (JVM)<br />

ชุดคำสั่งที่ 11.5 แสดงให้เห็นถึงการสร้าง MakeFile โดยไฟล์ที่สร้างขึ้นจะอยู่ในตำแหน่งเดียว<br />

กับโปรแกรมภาษา C ซึ่งมันจะเก็บรายละเอียดของ LOCAL_PATH และเรียกใช้ CLEAR_VARS เพื่อลบ<br />

ค่าตัวแปร LOCAL_* ก่อนที่จะทำการบิวด์ในแต่ละครั้ง และ LOCAL_MODULE จะกำหนดชื่อของ<br />

ไลบรารี ndkcookbook ที่สร้างขึ้น หลังจากที่กำหนดค่าเหล่านี้แล้ว มันก็จะถูกรวมไว้ใน BUILD_<br />

SHARED_LIBRARY ซึ่งจะเป็น MakeFile ที่ใช้ในการบิวด์แอพ รายละเอียดเกี่ยวกับการกำหนดค่าใน<br />

MakeFile จะกล่าวไว้ในไฟล์ <strong>ANDROID</strong>-MK.TXT ซึ่งอยู่ในไดเร็กทอรี docs/ ของ NDK


286 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

ชุดคำสั่งที่ 11.5 jni/Android.mk<br />

LOCAL_PATH := $(call my-dir)<br />

include $(CLEAR_VARS)<br />

LOCAL_MODULE := ndkcookbook<br />

LOCAL_SRC_FILES := cookbook.c<br />

include $(BUILD_SHARED_LIBRARY)<br />

ขั้นตอนต่อไปจะเป็นการบิวด์ไลบรารี ใน NDK-r4 มีสคริปต์ชื่อ ndk-build อยู่ในไดเร็กทอรี<br />

NDK ซึ่งจะใช้ในการบิวด์โปรเจ็กต์ รวมถึงไลบรารีอื่นๆ ที่เกี่ยวข้อง สำหรับใน NDK เวอร์ชั่นก่อนหน้านี้<br />

เราจะใช้คำสั่ง make <strong>APP</strong>=NAME_OF_<strong>APP</strong>LICATION แทน หลังจากที่บิวด์ไลบรารีแล้ว<br />

โฟลเดอร์ lib/ ก็จะถูกสร้างขึ้น และเก็บไฟล์ไลบรารี libndkcookbook.so เอาไว้ พร้อมทั้งมีไฟล์<br />

gdb จำนวน 2 ไฟล์เพื่อใช้ประโยชน์ในการดีบั๊กแอพด้วย<br />

แอคทิวิตี้ที่จะเรียกใช้งานไลบรารีนี้จะใช้คำสั่ง System.loadLibrary() เพื่อโหลดไลบรารี<br />

ndkcookbook ดังแสดงไว้ในชุดคำสั่งที่ 11.6 ซึ่งผลการทำงานของแอพจะแสดงไว้ในรูปที่ 11.2<br />

ชุดคำสั่งที่ 11.6 src/com/cookbook/advance/ndk/ndk.java<br />

package com.cookbook.advance.ndk;<br />

import android.app.Activity;<br />

import android.widget.TextView;<br />

import android.os.Bundle;<br />

public class ndk extends Activity {<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

TextView tv = new TextView(this);<br />

tv.setText(" native calculation on factorial :"+factorial(30));<br />

setContentView(tv);<br />

}<br />

public static native int factorial(int n);<br />

static {<br />

System.loadLibrary("ndkcookbook");<br />

}<br />

}


ระบบความปลอดภัยบนแอนดรอยด์<br />

287<br />

รูปที่ 11.2 ผลลัพธ์การทำางานของแอพ NDK<br />

ระบบความปลอดภัยบนแอนดรอยด์<br />

ระบบปฏิบัติการแอนดรอยด์มีลักษณะการทำงานแบบมัลติโปรเซสซิ่ง ซึ่งแอพแต่ละตัวจะทำงาน<br />

อยู่บน Android Dalvik Machine โดย Dalvik แต่ละตัวจะทำงานบนโปรเซสลีนุกซ์ แต่ละโปรเซสจะ<br />

ทำงานภายในขอบเขตของตนเอง นั่นหมายความว่าแอพจะสามารถเข้าถึงข้อมูลของมันเองได้เท่านั้น<br />

โดยปกติแล้วแอพแต่ละตัวจะมีค่า Linux User ID ซึ่งเราสามารถกำหนดให้แต่ละแอพใช้งาน<br />

Linux User ID เดียวกันได้ จึงทำให้แอพนั้นๆ มีสิทธิ์ในการใช้ทรัพยากรเช่นเดียวกัน<br />

เวลาเรียกใช้งานทรัพยากรที่อยู่นอกขอบเขตของแอพนั้น แอพจะต้องขออนุญาตจากระบบ<br />

ปฏิบัติการแอนดรอยด์ก่อน คอมโพเน็นต์ของระบบปฏิบัติการแอนดรอยด์มีอยู่หลายตัวที่จะต้องระบุ<br />

สิทธิ์ก่อนใช้งาน ซึ่งสิทธิ์ในการใช้งานนั้น เราจะประกาศไว้ในไฟล์ Manifest ที่จะใช้ในการยืนยันสิทธิ์<br />

ระหว่างที่ติดตั้งแอพ ถ้าผู้ใช้ยอมรับสิทธิ์ ระบบก็จะส่งผ่านสิทธิ์นี้ไปยังระบบปฏิบัติการ ซึ่งสิทธิ์จะถูก<br />

กำหนดไว้ใน android.Manifest.permission<br />

จากที่เคยกล่าวไว้ในบทที่ 1 แอพแต่ละตัวจะต้องมีการลงทะเบียนเพื่อสร้างใบรับรองของแอพ<br />

โดยข้อมูลนี้จะใช้ในการตรวจสอบว่าผู้ใดเป็นผู้พัฒนาแอพ แต่จะไม่ได้จัดการเกี่ยวกับสิทธิ์การเข้าถึง<br />

ของแอพ เราสามารถใช้แทค permission เพื่อกำหนดสิทธิ์ไว้ในไฟล์ AndroidManifest.xml ได้


288 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

กรรมวิธี: การประกาศและใช้ Permission<br />

สิทธิ์การทำงานนั้นสามารถกำหนดไว้ในแอคทิวิตี้, Broadcast Receiver, Content Provider<br />

และเซอร์วิสได้ ในการกำหนดสิทธิ์การทำงานนั้น เราจะต้องประกาศอีลีเมนต์ permission ใน<br />

AndroidManifest ดังนี้<br />

<br />

จากตัวอย่างข้างต้นจะเป็นการกำหนดสิทธิ์การทำงานเท่าที่ต้องการเท่านั้น แต่ในการเข้าถึง<br />

แอททริบิวต์ protectionLvel จะมีการเข้าถึงอยู่ 4 ระดับ ได้แก่ normal, dangerous, signature<br />

และ signatureOrSystem สำหรับแอทริบิวต์ permissionGroup จะใช้เพื่อแสดงสิทธิ์การทำงานให้<br />

แก่ผู้ใช้งานเท่านั้น ตัวอย่างการกำหนด PermissionGroup มีดังนี้<br />

permission group:android.permission-group.DEVELOPMENT_TOOLS<br />

permission group:android.permission-group.PERSONAL_INFO<br />

permission group:android.permission-group.COST_MONEY<br />

permission group:android.permission-group.LOCATION<br />

permission group:android.permission-group.MESSAGES<br />

permission group:android.permission-group.NETWORK<br />

permission group:android.permission-group.ACCOUNTS<br />

permission group:android.permission-group.STORAGE<br />

permission group:android.permission-group.PHONE_CALLS<br />

permission group:android.permission-group.HARDWARE_CONTROLS<br />

permission group:android.permission-group.SYSTEM_TOOLS<br />

แอททริบิวต์ของลาเบล, คำอธิบาย และชื่อเป็นวิธีที่จะช่วยให้ permission ชัดเจนมากขึ้น<br />

การสื่อสารข้อมูลระหว่างโปรเซสภายนอกของแอนดรอยด์<br />

ถ้าต้องการกำหนดให้แอพ 2 ตัวทำงานโดยใช้รีซอร์สร่วมกัน แต่ไม่สามารถกำหนดสิทธิ์ระหว่าง<br />

กันได้ เราจะใช้คำสั่ง IPC (Inter-Process Communication) มาช่วยในการทำงานนี้ การที่จะใช้งาน<br />

IPC นั้น เราจะต้องสร้างอินเตอร์เฟซระหว่างแอพขึ้นมา ทำได้โดยการใช้ AIDL (Android Interface<br />

Definition Language)<br />

AIDL จะมีลักษณะคล้ายกับจาวาอินเตอร์เฟซ สร้างได้ง่ายๆ ด้วยโปรแกรม Eclipse โดยเลือก<br />

คำสั่งสร้าง Java Interface หลังจากสร้างเสร็จแล้ว ระบบก็จะเปลี่ยนนามสกุลของไฟล์จาก .java<br />

ไปเป็น .aidl<br />

ชนิดของข้อมูลที่รองรับกับการทำงานของ AIDL มีดังนี้<br />

m Java Primitive เช่น int, Boolean, float<br />

m String<br />

m CharSequence


การสื่อสารข้อมูลระหว่างโปรเซสภายนอกของแอนดรอยด์<br />

289<br />

m List<br />

m Map<br />

m AIDL<br />

m คลาสที่สร้างขึ้นเองที่ใช้โปรโตคอล Parcelable ในการส่งค่าข้อมูล<br />

กรรมวิธี: การใช้งาน Remote Procedure Call<br />

ในหัวข้อนี้เราจะมาทดลองใช้งาน RPC (Remote Procedure Call) ระหว่างแอคทิวิตี้จำนวน<br />

2 ตัว ในขั้นแรก เราจะประกาศอินเตอร์เฟซ AIDL ตามที่แสดงในชุดคำสั่งที่ 11.7 ดังนี้<br />

ชุดคำสั่งที่ 11.7 IAdditionalService.aidl under the com.cookbook.advance.rpc.<br />

package com.cookbook.advance.rpc;<br />

// Declare the interface.<br />

interface IAdditionService {<br />

int factorial(in int value);<br />

}<br />

หลังจากที่ไฟล์ AIDL สร้างขึ้นแล้ว โปรแกรม Eclipse จะสร้างไฟล์ IAdditionalService.java<br />

ไว้ในโฟลเดอร์ gen/ เราจะไม่แก้ไขรายละเอียดของไฟล์นี้ เนื่องจากข้อมูลภายในไฟล์เป็นรายละเอียด<br />

การประกาศคลาสที่สามารถเรียกใช้งานโดย RPC ได้<br />

ภายในแอคทิวิตี้ชื่อ rpcService จะมีสมาชิกชื่อ mBinder ประกาศไว้ ซึ่งจะเป็นตัวเชื่อมโยง<br />

IAdditionalService ในเมธอด onCreate() คำสั่ง mBinder จะเริ่มทำงานเพื่อเรียกใช้ฟังก์ชั่น<br />

factorial() และหลังจากที่ onBind() ทำงานเสร็จ การทำงานของแอคทิวิตี้ก็สามารถเรียกใช้<br />

เซอร์วิสจากภายนอกได้ ดังแสดงไว้ในชุดคำสั่งที่ 11.8<br />

ชุดคำสั่งที่ 11.8 src/com/cookbook/advance/rpc/rpcService.java<br />

package com.cookbook.advance.rpc;<br />

import android.app.Service;<br />

import android.content.Intent;<br />

import android.os.IBinder;<br />

import android.os.RemoteException;<br />

public class RPCService extends Service {<br />

IAdditionService.Stub mBinder;<br />

@Override<br />

public void onCreate() {<br />

super.onCreate();<br />

mBinder = new IAdditionService.Stub() {<br />

public int factorial(int value1) throws RemoteException {<br />

int result=1;


290 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

}<br />

};<br />

for(int i=1; i


การสื่อสารข้อมูลระหว่างโปรเซสภายนอกของแอนดรอยด์<br />

291<br />

android:layout_height="wrap_content" android:text="result"<br />

android:textSize="36dp" android:id="@+id/result"><br />

<br />

ในชุดคำสั่งที่ 11.10 จะแสดงแอททริบิวต์ชื่อ android:process=”.remoteService”<br />

ซึ่งคำสั่งนี้จะสั่งให้ระบบทำการสร้างโปรเซสชื่อ remoteService เพื่อใช้ในการรันแอคทิวิตี้ที่ 2<br />

ชุดคำสั่งที่ 11.10 AndroidManifest.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

แอคทิวิตี้ตัวที่ 2 ที่แสดงในชุดคำสั่งที่ 11.11 นั้นจะต้องใช้คำสั่ง bindService() เพื่ออ่าน<br />

ข้อมูลจากฟังก์ชั่น factorial() โดยใช้ rpcService และคำสั่ง bindService() นี้จะต้องมี<br />

อินสแตนซ์สำหรับเชื่อมต่อเพื่อใช้เป็นอินเตอร์เฟซสำหรับตรวจสอบสถานะของแอพ<br />

คลาส myServiceConnection และคลาส IAdditionService จะสร้างอินสแตนซ์ไว้ใน<br />

แอคทิวิตี้ rpc ซึ่งคำสั่ง myServiceConnection จะคอยตรวจจับอีเวนต์ onServiceConnected<br />

และ onServiceDisconnected โดยที่ onServiceConnected จะส่งค่าของอินสแตนซ์ IBinder<br />

ไปยังอินสแตนซ์ IAdditionService และ onServiceDisconnected จะกำหนดค่าของ IAdditionService<br />

เป็น null<br />

ภายในแอคทิวิตี้ rpc มีการสร้างเมธอดจำนวน 2 ตัว คือ initService() ที่ใช้ในการเริ่มการ<br />

ทำงานของ myServiceConnection ซึ่งมันจะสร้างอินเท็นต์เพื่อกำหนดชื่อแพ็คเกจและชื่อคลาสเพื่อ<br />

ส่งค่าไปยัง bindService ด้วยอินสแตนซ์ myServiceConnection และกำหนดค่า BIND_AUTO_<br />

CREATE


292 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

หลังจากที่เซอร์วิสเชื่อมโยงกันแล้ว onServiceConnected ก็จะเรียกฟังก์ชั่นที่ต้องการใช้งาน<br />

และส่งผ่านค่า IBinder ไปยังอินสแตนซ์ IAdditionService ซึ่งแอคทิวิตี้ rpc ก็จะสามารถเรียก<br />

ใช้เมธอด factorial โดยแสดงผลลัพธ์การทำงานเหมือนในรูปที่ 11.3<br />

ชุดคำสั่งที่ 11.11 src/com/cookbook/advance/rpc/rpc.java<br />

package com.cookbook.advance.rpc;<br />

import android.app.Activity;<br />

import android.content.ComponentName;<br />

import android.content.Context;<br />

import android.content.Intent;<br />

import android.content.ServiceConnection;<br />

import android.os.Bundle;<br />

import android.os.IBinder;<br />

import android.os.RemoteException;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.widget.Button;<br />

import android.widget.EditText;<br />

import android.widget.TextView;<br />

import android.widget.Toast;<br />

public class rpc extends Activity {<br />

IAdditionService service;<br />

myServiceConnection connection;<br />

class myServiceConnection implements ServiceConnection {<br />

public void onServiceConnected(ComponentName name,<br />

IBinder boundService) {<br />

service = IAdditionService.Stub.asInterface((IBinder) boundService);<br />

Toast.makeText(rpc.this, "Service connected", Toast.LENGTH_SHORT)<br />

.show();<br />

}<br />

public void onServiceDisconnected(ComponentName name) {<br />

service = null;<br />

Toast.makeText(rpc.this, "Service disconnected", Toast.LENGTH_SHORT)<br />

.show();<br />

}<br />

}


การสื่อสารข้อมูลระหว่างโปรเซสภายนอกของแอนดรอยด์<br />

293<br />

private void initService() {<br />

connection = new myServiceConnection();<br />

Intent i = new Intent();<br />

i.setClassName("com.cookbook.advance.rpc",<br />

com.cookbook.advance.rpc.rpcService.class.getName());<br />

if(!bindService(i, connection, Context.BIND_AUTO_CREATE)) {<br />

Toast.makeText(rpc.this, "Bind Service Failed", Toast.LENGTH_LONG)<br />

.show();<br />

}<br />

}<br />

private void releaseService() {<br />

unbindService(connection);<br />

connection = null;<br />

}<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

initService();<br />

Button buttonCalc = (Button) findViewById(R.id.buttonCalc);<br />

buttonCalc.setOnClickListener(new OnClickListener() {<br />

TextView result = (TextView) findViewById(R.id.result);<br />

EditText value1 = (EditText) findViewById(R.id.value1);<br />

}<br />

public void onClick(View v) {<br />

int v1, res = -1;<br />

try {<br />

v1 = Integer.parseInt(value1.getText().toString());<br />

res = service.factorial(v1);<br />

} catch (RemoteException e) {<br />

e.printStackTrace();<br />

}<br />

result.setText(new Integer(res).toString());<br />

}<br />

});


294 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

}<br />

@Override<br />

protected void onDestroy() {<br />

releaseService();<br />

}<br />

รูปที่ 11.3 ผลลัพธ์จากการใช้งาน AIDL<br />

ตัวจัดการข้อมูลสำรองบนระบบปฏิบัติการแอนดรอยด์<br />

ในอุปกรณ์ที่ใช้ระบบปฏิบัติการแอนดรอยด์นั้น ผู้ใช้งานมักจะเก็บข้อมูลต่างๆ ไว้ในเครื่องเป็น<br />

จำนวนมาก ทั้งข้อมูลของแอพ เกม บันทึกต่างๆ รวมทั้งสมุดโทรศัพท์ ซึ่งหลังจากลบข้อมูลหรือข้อมูล<br />

สูญหายไปแล้วก็จะไม่สามารถกู้คืนได้ ในอดีตที่ผ่านมาผู้พัฒนาพยายามหาวิธีในการสำรองข้อมูลดัง<br />

กล่าวไปเก็บไว้ยังเซิร์ฟเวอร์ภายนอก โดยในระบบปฏิบัติการแอนดรอยด์เวอร์ชั่น 2.2 จะรองรับการ<br />

ทำงานร่วมกับ Android Backup Service ของGoogle ซึ่งข้อมูลที่สำรองไว้นั้นจะถูกเจ็ดเก็บไว้ใน<br />

ระบบคลาวด์ (Cloud)<br />

กรรมวิธี: การสร้างข้อมูลสำรองจากข้อมูลที่ใช้งาน<br />

เราสามารถใช้คลาส BackupManager ในการสำรองและกู้ข้อมูลคืนได้ ถ้ามีข้อความแจ้งไปยัง<br />

คลาสดังกล่าว BackupManager ก็จะสำรองข้อมูลจากแอพและส่งไปยังระบบคลาวด์ ในขณะเดียวกัน<br />

เราสามารถดึงข้อมูลจากคลาวด์ เพื่อส่งกลับมายังเครื่องและกู้ข้อมูลคืนได้ด้วย


ตัวจัดการข้อมูลสำารองบนระบบปฏิบัติการแอนดรอยด์<br />

295<br />

BackupManager จะทำงานร่วมกับแอพผ่านทาง Backup Agent ในการสร้าง Backup Agent<br />

นั้น ผู้พัฒนาสามารถใช้งานคลาส BackupAgent ภายในคลาสอื่นๆ ได้ ซึ่งมีเมธอดจำนวน 2 ตัวที่จะต้อง<br />

โอเวอร์ไรด์ คือ onBackup() จะเริ่มทำงานเมื่อเมธอด dataChanged() ทำงาน และ onRestore()<br />

จะเริ่มทำงานเมื่อเมธอด requestRestore() ทำงาน<br />

public class MyBackupAgent extends BackupAgent {<br />

@Override<br />

public void onCreate() {<br />

...<br />

}<br />

}<br />

@Override<br />

public void onBackup(ParcelFileDescriptor oldState,<br />

BackupDataOutput data,<br />

ParcelFileDescriptor newState){<br />

...<br />

}<br />

@Override<br />

public void onRestore(BackupDataInput data, int appVersionCode,<br />

ParcelFileDescriptor newState){<br />

...<br />

}<br />

ในเมธอด onBackup() จะมีค่าพารามิเตอร์จำนวน 3 ตัวที่จะส่งค่าไปยัง Backup Manager คือ<br />

m oldState – ส่งค่าสถานะการสำรองข้อมูลครั้งล่าสุด<br />

m data – ข้อมูลที่ต้องการสำรอง<br />

m newState – อัพเดตสถานะปัจจุบันของการสำรอง ในที่นี้ค่าสถานะนี้จะกลายเป็นค่า<br />

oldState ในการสำรองข้อมูลครั้งต่อไป<br />

เวลาใช้งานคำสั่ง onBackup() เราต้องตรวจสอบค่าของ oldState ก่อนที่จะส่งไปยัง<br />

BackupManager ถ้าค่าตรงกันก็ไม่ต้องสำรองข้อมูล แต่ถ้าไม่ตรงกัน พารามิเตอร์ data ก็จะถูกส่งไป<br />

ยัง Backup Manager และอัพเดตค่าของ newStat<br />

ในเมธอด onRestore() จะมีค่าพารามิเตอร์จำนวน 3 ตัวที่จะส่งค่าไปยัง BackupManager คือ<br />

m data – ข้อมูลที่สำรองครั้งล่าสุด<br />

m appVersionCode – เลขเวอร์ชั่นของแอพในระหว่างที่สำรองข้อมูล ซึ่งค่านี้จะกำหนดอยู่<br />

ในไฟล์ Manifest โดยใช้ชื่อแอททริบิวต์ว่า android:versionCode<br />

m newState() – เขียนสถานะปัจจุบันเพื่อใช้เป็นจุดกู้คืน (Restore Point)


296 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

ในกรณีที่เวอร์ชั่นของแอพไม่ตรงกัน และอาจมีชนิดของข้อมูลไม่ตรงกันนั้น คำสั่ง onRestore()<br />

จะแปลงข้อมูลให้โดยอัตโนมัติ ซึ่งเป็นเหตุผลที่ BackupManager จะต้องทำงานร่วมกับ<br />

appVersionCode หลังจากที่กู้ข้อมูลแล้ว สถานะของแอพก็จะเปลี่ยนไป และค่าของ newState ก็จะ<br />

ถูกอัพเดต<br />

กรรมวิธี: การสำรองข้อมูลไปไว้ที่คลาวด์<br />

BackupAgent ใช้ในการจัดเก็บข้อมูลของแอพในระหว่างที่แอพกำลังทำงาน สำหรับการจัดเก็บ<br />

ข้อมูลในรูปแบบไฟล์ เราจะใช้คำสั่ง BackupAgentHelper แทน ซึ่งคำสั่งนี้จะทำงานร่วมกับคลาส<br />

BackupAgent ที่รองรับการสำรองข้อมูล 2 ประเภทคือ<br />

m SharedPreferencesBackupHelper ใช้ในการสำรองข้อมูลไฟล์ SharedPreferences<br />

m FileBackupHelper ใช้ในการสร้างไฟล์ข้อมูลสำรอง<br />

ชุดคำสั่งที่ 11.12 ตัวอย่างของการใช้คำสั่ง BackupAgentHelper<br />

public class MyFileBackupAgentHelper extends BackupAgentHelper {<br />

@Override<br />

public void onCreate() {<br />

FileBackupHelper filehelper = new FileBackupHelper(this,<br />

DATA_FILE_NAME);<br />

addHelper(FILE_HELPER_KEY, helper);<br />

SharedPreferencesBackupHelper xmlhelper<br />

= new SharedPreferencesBackupHelper(this, PREFS);<br />

addHelper(PREFS_BACKUP_KEY, helper);<br />

}<br />

}<br />

BackupAgent ทุกตัวจะต้องเรียกใช้เมธอด onCreate() ซึ่ง BackupAgent สามารถมี<br />

BackupHelper ได้หลายตัว การสร้างอินสแตนซ์ของคลาส BackupAgentHelper ไม่จำเป็นต้อง<br />

โอเวอร์ไรด์เมธอด onBackup และ onRestore เพราะว่าการทำงานทั้งหมดถูกควบคุมโดย<br />

BackupAgent อยู่แล้ว<br />

กรรมวิธี: การสั่งให้สำรองและกู้คืนข้อมูล<br />

เราจะต้องกำหนดการสั่งให้แก่ BackupManager เพื่อให้สั่งสำรองและกู้คืนข้อมูลได้ ด้วยการ<br />

เพิ่มแอททริบิวต์ android:backupAgent ลงไปในไฟล์ Manifest ดังแสดงในชุดคำสั่งที่ 11.13 ดังนี้<br />

ชุดคำสั่งที่ 11.13 AndroidManifest.xml<br />

<br />


<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

ทุกๆ ครั้งที่แอพถูกสั่งให้สำรองหรือกู้คืนข้อมูลนั้น จะกระทำโดย BackupManager ซึ่งถูกเรียก<br />

ใช้โดย BackupAgent ดังตัวอย่างด้านล่างนี้<br />

public class MyBandRActivity extends Activity {<br />

BackupManager mBackupManager;<br />

ตัวจัดการข้อมูลสำารองบนระบบปฏิบัติการแอนดรอยด์<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

...<br />

mBackupManager = new BackupManager(this);<br />

}<br />

297<br />

}<br />

void dataUpdate() {<br />

...<br />

// We also need to perform an initial backup; ask for one<br />

mBackupManager.dataChanged();<br />

}<br />

ภายในแอคทิวิตี้ myBandRActivity จะมีการสร้างอินสแตนซ์ของ BackupManager ในฟังก์ชั่น<br />

onCreate() การสำรองข้อมูลนั้น ฟังก์ชั่น dataChanged() จะต้องถูกเรียกจาก BackupManager<br />

เมื่อ BackupManager พบว่า BackupAgent ได้ถูกกำหนดไว้ในไฟล์ AndroidManifest แล้ว ก็จะ<br />

เรียกใช้คำสั่ง onBackup()<br />

การกู้คืนข้อมูลนั้นจะมีวิธีการอยู่ 2 แบบที่จะสั่งให้ทำงาน วิธีแรกคือใช้เมธอด requestRestore()<br />

จาก BackupManager คำสั่งนี้จะเรียกใช้เมธอด onRestore() ส่วนวิธีที่ 2 จะเรียกใช้งาน<br />

เมื่อมีการรีเซ็ตค่าของอุปกรณ์กลับไปสู่ค่าเริ่มต้น ในกรณีนี้ระบบปฏิบัติการแอนดรอยด์จะกู้คืนข้อมูล<br />

กลับมา<br />

การสำรองและกู้คืนข้อมูลนั้น แอนดรอยด์มีคำสั่งที่ใช้ในคอมมานด์ไลน์ คือคำสั่ง bmgr<br />

ซึ่งทำงานเหมือนกับ BackupManager ถ้าต้องการสำรองข้อมูลก็ให้ใช้คำสั่งดังนี้<br />

> adb shell bmgr backup <br />

และถ้าต้องการจะกู้คืนข้อมูลก็จะใช้คำสั่งดังนี้<br />

> adb shell bmgr restore


298 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

โดยปกติแล้ว BackupManager อาจไม่ทำงานทันที ถ้าในระหว่างนั้นยังมีการทำงานอื่นๆ มา<br />

ขัดจังหวะ เราสามารถกำหนดให้ BackupManager ทำงานทันทีโดยไม่ต้องรอให้งานดังกล่าวเสร็จได้<br />

โดยใช้คำสั่งนี้<br />

> adb shell bmgr run<br />

การแสดงภาพเคลื่อนไหวบนระบบปฏิบัติการแอนดรอยด์<br />

การแสดงภาพเคลื่อนไหวในระบบปฏิบัติการแอนดรอยด์ทำได้ 2 วิธี คือ แบบเฟรมต่อเฟรม<br />

และแบบทวีน (Tween) ซึ่งในวิธีแรกนั้นจะแสดงภาพไปเรื่อยๆ ตามลำดับ โดยผู้พัฒนาจะต้องเตรียม<br />

ชุดภาพที่จะใช้แสดง มีลักษณะการทำงานคล้ายการโชว์ภาพสไลด์<br />

การแสดงภาพเคลื่อนไหวแบบเฟรมต่อเฟรมนั้น เราต้องกำหนด animation-list ลงไปในไฟล์<br />

เลย์เอาต์ และกำหนดค่าของอีลีเมนต์ item แทนลำดับของรูปภาพที่จะแสดงผล แอททริบิวต์ oneshot<br />

จะใช้เมื่อต้องการกำหนดให้เล่นรูปภาพรอบเดียวหรือหลายรอบ ตัวอย่างการกำหนดเลย์เอาต์เพื่อแสดง<br />

ภาพเคลื่อนไหวจะแสดงอยู่ในชุดคำสั่งที่ 11.14 ดังนี้<br />

ชุดคำสั่งที่ 11.14 res/anim/animated.xml<br />

<br />

<br />

<br />

<br />

<br />

<br />

การแสดงภาพแบบเฟรมต่อเฟรมนั้น เราต้องกำหนดภาพเคลื่อนไหวนี้ไว้ในพื้นหลังของวิว<br />

ImageView im = (ImageView) this.findViewById(R.id.myanimated);<br />

im.setBackgroundResource(R.anim.animated);<br />

AnimationDrawable ad = (AnimationDrawable)im.getBackground();<br />

ad.start();<br />

หลังจากที่กำหนดพื้นหลังของวิวแล้ว ก็จะเรียกใช้คำสั่ง getBackground() เพื่อแสดงออบเจ็กต์<br />

AnimationDrawable และขั้นตอนต่อไปก็จะเรียกคำสั่ง start() เพื่อเริ่มต้นเล่นภาพเคลื่อนไหว<br />

ส่วนการแสดงภาพเคลื่อนไหวโดยใช้ทวีนจะเป็นการแสดงภาพโดยกำหนดจุดเริ่มต้นและจุดสิ้น<br />

สุด ระบบจะวาดภาพที่เกิดขึ้นในระหว่างจุดดังกล่าวให้เอง ซึ่งในระบบปฏิบัติการแอนดรอยด์มีคลาสให้<br />

เลือกใช้งานดังนี้<br />

m AlphaAnimation – ควบคุมความโปร่งแสงของออบเจ็กต์<br />

m RotateAnimation – ควบคุมการหมุนของออบเจ็กต์<br />

m ScaleAnimation – ควบคุมการย่อ-ขยายของออบเจ็กต์<br />

m TranslateAnimation – ควบคุมตำแหน่งของออบเจ็กต์


การแสดงภาพเคลื่อนไหวบนระบบปฏิบัติการแอนดรอยด์<br />

299<br />

คลาสทั้ง 4 คลาสนี้สามารถนำมาใช้สลับการทำงานระหว่างแอคทิวิตี้ วิว และเลย์เอาต์ได้ โดยที่<br />

เราสามารถกำหนดค่าเหล่านี้ไว้ในไฟล์ XML ตัวอย่างเช่น , , และ<br />

โดยค่าเหล่านี้จะต้องกำหนดไว้ภายใต้ AnimationSet <br />

m แอททริบิวต์ <br />

android:fromAlpha, android:toAlpha<br />

ค่าโปร่งแสงของออบเจ็กต์ ซึ่งมีค่าตั้งแต่ 0.0 (โปร่งแสง) ถึง 1.0 (ทึบแสง)<br />

m แอททริบิวต์ <br />

android:fromDegrees, android:toDegrees,<br />

android:pivotX, android:pivotY<br />

กำหนดค่าการหมุนของออบเจ็กต์ มีหน่วยเป็นองศา โดยจะหมุนรอบจุดศูนย์กลางที่<br />

กำหนดให้เป็น pivot<br />

m แอททริบิวต์ <br />

android:fromXScale, android:toXScale,<br />

android:fromYScale, android:toYScale,<br />

android:pivotX, android:pivotY<br />

กำหนดค่าการเปลี่ยนขนาดของออบเจ็กต์ในแนวแกน x และ y ซึ่ง pivot จะแทนจุดที่จะ<br />

ยึดออบเจ็กต์ไว้<br />

m แอททริบิวต์ <br />

android:fromXDelta, android:toXDelta,<br />

android:fromYDelta, android:toYDelta<br />

กำหนดค่าการเปลี่ยนตำแหน่งของออบเจ็กต์<br />

กรรมวิธี: การสร้างภาพเคลื่อนไหว<br />

ในหัวข้อนี้จะสร้างภาพเคลื่อนไหวที่แสดงว่ามีเมล์ใหม่ส่งเข้ามา ซึ่งจะแสดงภาพนี้เมื่อได้รับเมล์<br />

โดยในชุดคำสั่งที่ 11.15 จะแสดงเลย์เอาต์ของชุดคำสั่งนี้เอาไว้ ส่วนผลลัพธ์ที่ได้จะอยู่ในรูปที่ 11.4<br />

ชุดคำสั่งที่ 11.15 res/layout/main.xml<br />

<br />

<br />


300 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

android:src="@drawable/mail"<br />

/><br />

<br />

<br />

รูปที่ 11.5 แสดงเลย์เอาต์ของภาพเคลื่อนไหว<br />

ในการแสดงภาพเคลื่อนไหวในวิวนี้ เราจะต้องกำหนดค่าของภาพเคลื่อนไหวลงไปในไฟล์ XML<br />

ด้วย ทำได้โดยไปที่โปรแกรม Eclipse คลิกขวาที่โฟลเดอร์ res/ และเลือกคำสั่ง New → android<br />

XML File และกำหนดชื่อของไฟล์เป็น animated.xml และเลือกชนิดของไฟล์เป็น Animation<br />

หลังจากนั้นเราก็แก้ไขไฟล์เพื่อสร้างคอนเทนต์ ตามที่แสดงในชุดคำสั่งที่ 11.16<br />

ชุดคำสั่งที่ 11.16 res/anim/animated.xml<br />

<br />

<br />


การแสดงภาพเคลื่อนไหวบนระบบปฏิบัติการแอนดรอยด์<br />

android:duration="5000" /><br />

<br />

<br />

301<br />

<br />

<br />

แอคทิวิตี้หลักที่แสดงในชุดคำสั่งที่ 11.17 เป็นแอคทิวิตี้ที่สร้างออบเจ็กต์ Animation โดยใช้<br />

คำสั่ง AnimationUtils ในการโหลดข้อมูล animationSet เพื่อใช้เล่นภาพเคลื่อนไหว ทุกครั้งที่ผู้ใช้<br />

กดปุ่ม ออบเจ็กต์วิวก็จะเล่นภาพเคลื่อนไหวโดยเรียกใช้คำสั่งเมธอด startAnimation()<br />

ชุดคำสั่งที่ 11.17 src/com/cookbook/advance/myanimation.java<br />

package com.cookbook.advance;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.view.View;<br />

import android.view.View.OnClickListener;<br />

import android.view.animation.Animation;<br />

import android.view.animation.AnimationUtils;<br />

import android.widget.Button;<br />

import android.widget.ImageView;<br />

public class myanimation extends Activity {<br />

/** Called when the activity is first created. */<br />

@Override


302 บทที่ 11 เทคนิคขั้นสูงสำาหรับพัฒนาแอพบนแอนดรอยด์<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

final ImageView im<br />

= (ImageView) this.findViewById(R.id.myanimated);<br />

final Animation an<br />

= AnimationUtils.loadAnimation(this, R.anim.animated);<br />

im.setVisibility(View.INVISIBLE);<br />

Button bt = (Button) this.findViewById(R.id.startAnimated);<br />

bt.setOnClickListener(new OnClickListener(){<br />

public void onClick(View view){<br />

im.setVisibility(View.VISIBLE);<br />

im.startAnimation(an);<br />

}<br />

}<br />

}<br />

});


303<br />

บทที่ 12<br />

การตรวจสอบการทำงานของแอพ<br />

การตรวจสอบหรือดีบั๊ก (Debug) เพื่อหาข้อผิดพลาดในแอพนั้น บางครั้งอาจใช้เวลานานกว่า<br />

เวลาที่ใช้พัฒนาแอพเสียอีก การที่เราเข้าใจกลไกการดีบั๊กจะช่วยให้ค้นพบปัญหาได้อย่างรวดเร็วและ<br />

ประหยัดเวลา ในบทนี้จะพูดถึงการใช้เครื่องมือดีบั๊กแอพ โดยในช่วงแรกนี้จะว่าด้วยการใช้เครื่องมือ<br />

ของโปรแกรม Eclipse ที่มาพร้อมกับ Android SDK อยู่แล้ว จากนั้นจะพูดถึงเครื่องมืออื่นๆ ที่ทำงาน<br />

ประเภทเดียวกัน แอพแต่ละตัวมีการทำงานแตกต่างกัน ดังนั้นเวลาจะดีบั๊กแอพเพื่อหาข้อผิดพลาดก็<br />

ควรเลือกวิธีที่เหมาะสมกับโครงสร้างการทำงานนั้นๆ ด้วย<br />

เครื่องมือดีบั๊กแอพของ Eclipse<br />

การพัฒนาแอพบนแอนดรอยด์โดยใช้โปรแกรม Eclipse IDE ร่วมกับ ADT (Android<br />

Developer Tools) นั้น นับว่าเป็นสภาพแวดล้อมที่เหมาะสมสำหรับการพัฒนาแอพพลิเคชั่น เพราะใช้<br />

งานง่าย ไม่ซับซ้อน อีกทั้งยังมีการสร้างอินเตอร์เฟซและเลย์เอาต์แบบประเภทเห็นอย่างไรก็ได้อย่าง<br />

นั้น หรือที่เรียกกันว่า WYSIWYG (What-You-See-Is-What-You-Get) ด้วย ซึ่งในหัวข้อนี้เราจะ<br />

อ้างอิงถึงการทำงานของ Eclipse 3.4 (Ganymede) เพื่อใช้ในการอธิบายขั้นตอนต่างๆ สำหรับ<br />

โปรแกรม Eclipse เวอร์ชั่นอื่นๆ นั้นก็จะมีการทำงานที่ไม่ต่างกันมากนัก<br />

กรรมวิธี: การกำหนดค่าที่ใช้ในการทำงาน (Run Configuration)<br />

Run Configuration เป็นโปรไฟล์ที่แยกออกมาจากแต่ละแอพ ทำหน้าที่บอกให้โปรแกรม<br />

Eclipse รู้ถึงวิธีการรันโปรเจ็กต์ การทำงานของแอคทิวิตี้ และการติดตั้งแอพว่าจะติดตั้งไว้ในระบบ<br />

จำลองการทำงานหรือติดตั้งไว้ในอุปกรณ์โดยตรง ADT จะสร้าง Run Configuration ให้กับแต่ละแอพ<br />

โดยอัตโนมัติ หลังจากสร้างแล้วเราสามารถเข้าไปแก้ไขค่าต่างๆ ในภายหลังได้<br />

การสร้าง Run Configuration หรือแก้ไขค่าที่มีอยู่แล้ว ทำได้โดยเลือกเมนู Run → Run<br />

Configurations (หรือ Debug Configurations) เพื่อเปิดหน้าจอการกำหนด Run Configurations<br />

ดังแสดงในรูปที่ 12.1 ขึ้นมา ซึ่งในหน้าต่างนี้จะมีแท็บอยู่ 3 แท็บที่เกี่ยวข้องกับการทดสอบแอพ ดังนี้


304 บทที่ 12 การตรวจสอบการทำางานของแอพ<br />

m Android – กำหนดโปรเจ็กต์และแอคทิวิตี้ที่จะเรียกใช้งาน<br />

m Target – กำหนดค่าของอุปกรณ์เสมือนที่จะใช้ในการทดสอบการทำงานของแอพ<br />

ส่วนการใช้งานในระบบจำลองนั้น ในส่วนนี้เราจะกำหนดค่าที่ใช้เริ่มการทำงานของแอพ<br />

และความเร็วของอินเตอร์เน็ตเพื่อแสดงสภาพการทำงานของแอพ ซึ่งผู้พัฒนาสามารถ<br />

ลบค่าต่างๆ ทุกครั้งที่ระบบจำลองเริ่มทำงานได้<br />

m Common – กำหนดตำแหน่งของ Run Configuration ที่จะใช้งาน ซึ่งจะแสดงอยู่ในเมนู<br />

Favorite ด้วย<br />

รูปที่ 12.1 แสดงเมนูของ Run Configuration ในโปรแกรม Eclipse<br />

หลังจากที่กำหนดค่าต่างๆ ที่เกี่ยวข้องแล้ว ก็ถึงเวลาสั่งให้แอพพลิเคชั่นเริ่มทำงานบนระบบที่<br />

กำหนดไว้ โดยให้คลิกที่ปุ่ม Run ถ้าเราไม่ได้ต่ออุปกรณ์แอนดรอยด์กับคอมพิวเตอร์เอาไว้ ระบบก็จะ<br />

เลือกระบบจำลองขึ้นมาทำงานให้แทน<br />

กรรมวิธี: การใช้งาน DDMS<br />

หลังจากที่แอพเริ่มทำงานบนอุปกรณ์แอนดรอยด์หรือระบบจำลองแล้ว ให้เปิดหน้าต่างของ<br />

DDMS (Dalvik Debug Monitoring Service) เพื่อตรวจสอบสถานะของอุปกรณ์ดังแสดงในรูปที่<br />

12.2 นอกจากนี้ เรายังสามารถสั่งให้ DDMS ทำงานด้วยการสั่งที่คอมมานด์ไลน์หรือเลือกที่เมนูของ<br />

Eclipse ได้ด้วยโดยเข้าไปที่เมนู Window → Open Perspective → DDMS


เครื่องมือดีบั๊กแอพของ Eclipse<br />

305<br />

รูปที่ 12.2 หน้าจอของ DDMS<br />

ภายในหน้าต่าง DDMS จะประกอบไปด้วยพาเนล 4 ส่วนที่แสดงข้อมูลเกี่ยวกับการดีบั๊ก ดังนี้<br />

m Devices – แสดงหน้าจอของระบบที่เชื่อมต่อ อาจเป็นระบบจำลองหรืออุปกรณ์<br />

แอนดรอยด์<br />

m Emulator Control – ใช้ควบคุมการทำงานของระบบจำลองเพื่อสร้างอีเวนต์ต่างๆ หรือ<br />

กำหนดข้อมูลเพื่อทดสอบการทำงาน เช่น<br />

m สถานะของระบบโทรศัพท์ - กำหนดรูปแบบของเสียงเรียกเข้า เครือข่ายโทรศัพท์<br />

ความเร็วของเครือข่าย<br />

m การทำงานของระบบโทรศัพท์ - ใช้ทดสอบการจำลองเวลามีสายเรียกเข้า หรือการ<br />

ใช้งานระบบข้อความสั้น (SMS)<br />

m การทำงานของระบบตรวจสอบตำแหน่ง - ใช้จำลองการทำงานของระบบ GPS<br />

ภายในระบบจำลอง<br />

m Bottom Panel – ประกอบไปด้วยแท็บ 3 แท็บ คือ LogCat ใช้แสดงข้อมูลภายใน<br />

อุปกรณ์แบบรีลไทม์, Outline และ Properties โดยพาเนลนี้จะแสดงข้อมูลเกี่ยวกับ Log<br />

ต่างๆ ที่เกิดขึ้นในระหว่างที่แอพกำลังทำงาน ซึ่งเราสามารถเข้าถึง Log เหล่านี้ได้โดยใช้<br />

คลาส Log ในแอพ<br />

m Device Status Panel - ประกอบไปด้วยแท็บ 4 แท็บ คือ Thread, Heap, Allocation<br />

Tracker และ File Explorer ใช้ในการตรวจดูการทำงานของเธรด ค่าตัวแปรต่างๆ<br />

รวมถึงการใช้หน่วยความจำด้วย ดังแสดงในรูปที่ 12.3


306 บทที่ 12 การตรวจสอบการทำางานของแอพ<br />

รูปที่ 12.3 หน้าต่างของ DDMS ในส่วนของ Device Status Panel<br />

กรรมวิธี: การตรวจสอบการทำงานของแอพด้วยการกำหนดจุดหยุด<br />

การทำงาน<br />

ผู้พัฒนาสามารถสั่งให้แอพทำงานในโหมดดีบั๊ก และกำหนดจุดหยุดต่างๆ (Break Point)<br />

เพื่อใช้หยุดการทำงานในแต่ละช่วง ในขั้นตอนแรกแอพจะต้องถูกกำหนดให้ทำงานในโหมดดีบั๊กก่อน<br />

ดังแสดงในรูปที่ 12.4 ถ้าเราเลือกที่ Yes ระบบก็จะแสดงหน้าจอ Debug Perspective ดังรูปที่ 12.5<br />

หน้าจอ Debug Perspective จะแสดงข้อมูลของซอร์สไฟล์อันประกอบไปด้วชุดคำสั่งของแอพ<br />

และการกำหนดจุด Break Point ต่างๆ ผู้ใช้งานสามารถสลับการทำงานของ Break Point ได้โดย<br />

ดับเบิ้ลคลิกที่ขอบด้านซ้ายของคำสั่ง ที่ซึ่งแอพหยุดการทำงานไว้ที่จุดนั้น จุด Break Point จะทำงาน<br />

เมื่อมีจุดสีน้ำเงินแสดงที่บรรทัดคำสั่งนั้น<br />

การใช้งาน Break Point เป็นวิธีการดีบั๊กหาข้อผิดพลาดที่ใช้ในการพัฒนาแอพโดยทั่วไป ซึ่งเรา<br />

สามารถหยุดการทำงานในแต่ละส่วนเพื่อตรวจดูค่าตัวแปรต่างๆ รวมทั้งทดสอบการแก้ไขค่าตัวแปรใน<br />

ระหว่างที่แอพกำลังทำงานได้ นับเป็นวิธีที่ดีที่สุดในการหาข้อผิดพลาดในแอพ<br />

รูปที่ 12.4 หน้าจอไดอะล็อกบ็อกซ์ยืนยันการการเปลี่ยน Perspective


เครื่องมือดีบั๊กแอพของชุดพัฒนาโปรแกรม Android SDK<br />

307<br />

รูปที่ 12.5 หน้าจอ Debug Perspective ในโปรแกรม Eclipse<br />

เครื่องมือดีบั๊กแอพของชุดพัฒนาโปรแกรม Android SDK<br />

Android SDK มีเครื่องมือที่ใช้การในดีบั๊กแอพเช่นกัน ประกอบไปด้วย Android Debug<br />

Bridge, LogCat, Hierarchy Viewer และ TraceView โดยเครื่องมือเหล่านี้จะอยู่ในไดเร็กทอรี<br />

tools/ ของ Android SDK<br />

กรรมวิธี: การใช้งาน Android Debug Bridge<br />

การใช้งาน Android Debug Bridge (ADB) เป็นวิธีการหนึ่งที่ช่วยควบคุมสถานะของระบบ<br />

จำลอง หรืออุปกรณ์แอนดรอยด์ที่ต่อไว้กับคอมพิวเตอร์ ADB จะประกอบไปด้วยคอมโพเน็นต์ 3 ตัว<br />

คือ Client, Server และ Daemon ซึ่งคอมโพเน็นต์ Client จะถูกเรียกใช้โดย ADB Shell Script<br />

บนเครื่องที่ใช้พัฒนาแอพ ส่วนคอมโพเน็นต์ Server จะทำงานอยู่เบื้องหลังในเครื่องที่ใช้พัฒนาแอพ<br />

เราสามารถสั่งให้คอมโพเน็นต์ Server ทำงานและหยุดทำงานได้โดยใช้คำสั่งดังนี้<br />

> adb start-server<br />

> adb kill-server<br />

คอมโพเน็นต์ Deamon จะทำงานอยู่เบื้องหลัง ซึ่งจะใช้ในการทำงานของระบบจำลองหรือ<br />

อุปกรณ์แอนดรอยด์<br />

กรรมวิธี: การใช้งาน LogCat<br />

LogCat เป็นเครื่องมือที่ใช้ในการตรวจสอบข้อมูล Log แบบรีลไทม์ มันจะรวบรวมข้อมูล Log<br />

ของระบบปฏิบัติการและแอพเอาไว้ ซึ่งเราสามารถดูหรือคัดกรองข้อมูลเหล่านี้ได้ นอกจากนี้ เรายังใช้<br />

งาน LogCat แบบ Stand-alone หรือใช้งานร่วมกับ DDMS ได้ด้วย


308 บทที่ 12 การตรวจสอบการทำางานของแอพ<br />

LogCat สามารถใช้งานกับอุปกรณ์หลังจากที่มีการเรียกใช้คำสั่ง adb shell หรือเรียกใช้<br />

LogCat จากคอมมานด์ไลน์โดยตรงได้ดังนี้<br />

> [adb] logcat [] ... [] ...<br />

ข้อความต่างๆ ที่เกิดจากการทำงานของคลาส android.util.Log จะมีข้อมูลแท็กและลำดับความ<br />

สำคัญกำหนดไว้เสมอ โดยข้อมูลแท็กจะอธิบายถึงความหมายและสิ่งแอคทิวิตี้ที่กำลังทำ จากราย<br />

ละเอียดของข้อมูลนี้จะช่วยให้เราสามารถดูและคัดกรองข้อมูล Log ได้ง่ายขึ้น โดยข้อมูลแท็กที่ใช้มี<br />

ดังนี้<br />

m V – Verbose (ลำดับความสำคัญต่ำสุด)<br />

m D - Debug<br />

m I - Info<br />

m W – Warning<br />

m E - Error<br />

m F - Fatal<br />

m S – Silent (ลำดับความสำคัญสูงสุด)<br />

ข้อมูลของ LogCat มีรายละเอียดที่หลากหลาย เราจะต้องคัดกรองข้อมูลก่อนด้วยการกำหนด<br />

อาร์กิวเมนต์ tag:priority ให้แก่คำสั่ง LogCat ดังนี้<br />

> adb logcat ActivityManager:V *:S<br />

จากตัวอย่างข้างต้นจะแสดงข้อมูลของ ActivityManager ที่มีค่าแท็กเป็น V และแสดงข้อมูลที่<br />

มีแท็กเป็น S ทุกตัว<br />

การใช้งานบัฟเฟอร์แบบวงกลมนั้นมีการใช้ในระบบ Log ของระบบปฏิบัติการแอนดรอยด์<br />

โดยปกติแล้ว Log ทั่วไปจะถูกเก็บลงในบัฟเฟอร์ Main Log แต่ใน Android SDK 2.2 จะมีการใช้<br />

งานบัฟเฟอร์ 2 ส่วน โดยส่วนแรกจะเก็บข้อมูลของระบบโทรศัพท์และอุปกรณ์ไร้สายต่างๆ ขณะที่อีก<br />

ส่วนหนึ่งจะเก็บข้อมูลเกี่ยวกับเหตุการณ์ต่างๆ การใช้งานบัฟเฟอร์แบบแยกนี้สามารถใช้งานโดยใช้<br />

พารามิเตอร์ –b ได้ดังนี้<br />

> adb logcat -b events<br />

บัฟเฟอร์นี้จะแสดง Log ที่เกี่ยวข้องกับเหตุการณ์ต่างๆ<br />

I/menu_opened( 135): 0<br />

I/notification_cancel( 74): [com.android.phone,1,0]<br />

I/am_finish_activity( 74):<br />

[1128378040,38,com.android.contacts/.DialtactsActivity,app-request]<br />

I/am_pause_activity( 74):<br />

[1128378040,com.android.contacts/.DialtactsActivity]<br />

I/am_on_paused_called( 135): com.android.contacts.RecentCallsListActivity<br />

I/am_on_paused_called( 135): com.android.contacts.DialtactsActivity<br />

I/am_resume_activity( 74): [1127710848,2,com.android.launcher/.Launcher]<br />

I/am_on_resume_called( 135): com.android.launcher.Launcher<br />

I/am_destroy_activity( 74):<br />

[1128378040,38,com.android.contacts/.DialtactsActivity]<br />

I/power_sleep_requested( 74): 0<br />

I/power_screen_state( 74): [0,1,468,1]


เครื่องมือดีบั๊กแอพของชุดพัฒนาโปรแกรม Android SDK<br />

I/power_screen_broadcast_send( 74): 1<br />

I/screen_toggled( 74): 0<br />

I/am_pause_activity( 74): [1127710848,com.android.launcher/.Launcher]<br />

309<br />

และด้านล่างนี้คืออีกหนึ่งตัวอย่าง<br />

> adb logcat -b radio<br />

ชุดคำสั่งนี้จะแสดงข้อความที่เกี่ยวข้องกับ radio/telephony<br />

D/RILJ ( 132): [2981]< GPRS_REGISTRATION_STATE {1, null, null, 2}<br />

D/RILJ ( 132): [2982]< REGISTRATION_STATE {1, null, null, 2, null, null,<br />

null, null, null, null, null, null, null, null}<br />

D/RILJ ( 132): [2983]< QUERY_NETWORK_SELECTION_MODE {0}<br />

D/GSM ( 132): Poll ServiceState done: oldSS=[0 home T - Mobile T - Mobile<br />

31026 Unknown CSS not supported -1 -1RoamInd: -1DefRoamInd: -1]<br />

newSS=[0 home T - Mobile T - Mobile 31026 Unknown CSS not supported -1 -<br />

1RoamInd: -1DefRoamInd: -1] oldGprs=0 newGprs=0 oldType=EDGE newType=EDGE<br />

D/RILJ ( 132): [UNSL]< UNSOL_NITZ_TIME_RECEIVED 10/06/26,21:49:56-28,1<br />

I/GSM ( 132): NITZ: 10/06/26,21:49:56-28,1,237945599 start=237945602<br />

delay=3<br />

D/RILJ ( 132): [UNSL]< UNSOL_RESPONSE_NETWORK_STATE_CHANGED<br />

D/RILJ ( 132): [2984]> OPERATOR<br />

D/RILJ ( 132): [2985]> GPRS_REGISTRATION_STATE<br />

D/RILJ ( 132): [2984]< OPERATOR {T - Mobile, T - Mobile, 31026}<br />

D/RILJ ( 132): [2986]> REGISTRATION_STATE<br />

D/RILJ ( 132): [2987]> QUERY_NETWORK_SELECTION_MODE<br />

D/RILJ ( 132): [2985]< GPRS_REGISTRATION_STATE {1, null, null, 2}<br />

D/RILJ ( 132): [2986]< REGISTRATION_STATE {1, null, null, 2, null, null,<br />

null, null, null, null, null, null, null, null}<br />

D/RILJ ( 132): [2987]< QUERY_NETWORK_SELECTION_MODE {0}<br />

Logcat จะทำงานได้ดีกับแอพที่พัฒนาด้วยภาษาจาวา แต่แอพที่เขียนขึ้นนั้นจะทำงานร่วมกับ<br />

คอมโพเน็นต์ภาษา C/C++ ซึ่งอาจจะยุ่งยากในการตรวจสอบ ในกรณีนี้เราต้องตรวจสอบการทำงาน<br />

ของคอมโพเน็นต์ดังกล่าวด้วยคำสั่ง system.out หรือ system.err แทน โดยปกติแล้วระบบปฏิบัติการ<br />

แอนดรอยด์จะส่งข้อมูล stdout และ stderr ไว้ในไดเร็กทอรี /dev/null/ เราสามารถใช้คำสั่ง ADB<br />

เพื่อเข้าถึง Log ดังกล่าวได้<br />

> adb shell stop<br />

> adb shell setprop log.redirect-stdio true<br />

> adb shell start<br />

การทำงานของคำสั่งนี้จะหยุดการทำงานของระบบจำลองหรืออุปกรณ์แอนดรอยด์ เราจึงต้องใช้<br />

คำสั่ง setprop เพื่อเปิดการทำงาน และเริ่มการทำงานของระบบจำลองใหม่<br />

กรรมวิธี: การใช้งาน Hierarchy Viewer<br />

วิธีที่จะช่วยให้เข้าใจถึงขั้นตอนการดีบั๊กคำสั่งและการทำงานของ UI นั้น Hierarchy Viewer<br />

ช่วยคุณได้ เพราะจะแสดงข้อมูลในแบบเลย์เอาต์กราฟิก


310 บทที่ 12 การตรวจสอบการทำางานของแอพ<br />

เราสามารถใช้งาน Hierarchy Viewer ได้ด้วยการใช้เครื่องมือ hierarchyviewer โดยจะแสดง<br />

หน้าจอตามรูปที่ 12.6 ซึ่งแสดงรายการของอุปกรณ์แอนดรอยด์ที่กำลังเชื่อมต่ออยู่กับเครื่องที่ใช้<br />

พัฒนาแอพ และเมื่อเลือกอุปกรณ์แล้วก็จะแสดงรายของแอพที่ทำงานอยู่บนอุปกรณ์นั้น โดยที่เรา<br />

สามารถเลือกแอพที่ต้องการดีบั๊กหรือตรวจสอบการทำงานของ UI ได้<br />

รูปที่ 12.6 หน้าจอของ Hierarchy Viewer<br />

หลังจากที่เลือกแอพแล้วเราสามารถเลือก LoadView Hierarchy เพื่อดูวิวที่ใช้งานอยู่ได้ ซึ่งจะ<br />

มีลักษณะเป็นแผนภูมิแบบต้นไม้ที่สร้างโดย Hierarchy Viewer โดยมุมมองของวิวนี้จะมี 3 แบบดังนี้<br />

m Tree View – แสดงไดอะแกรมของวิว ซึ่งจะอยู่ทางด้านซ้าย<br />

m Properties View – แสดงรายการของ Properties ของวิว ซึ่งจะอยู่ทางด้านบนขวา<br />

m Wire-Frame View – แสดงเลย์เอาต์แบบ Wire-Frame ซึ่งจะอยู่ทางด้านล่างขวา<br />

ดังที่แสดงไว้ในรูปที่ 12.7<br />

วิวทั้ง 3 นี้จะทำงานสัมพันธ์กัน เมื่อเลือกโหลดวิวใดๆ ค่าในส่วนของ Properties และ<br />

Wire-Frame ก็จะมีการเปลี่ยนแปลงด้วย สำหรับ Tree View นั้นจะมีข้อจำกัดตรงที่ขนาดของ Tree<br />

ห้ามลึกเกินกว่า 10 ชั้น และห้ามกว้างเกินกว่า 50 โหนด (Node) มิฉะนั้นถ้านำไปใช้งานร่วมกับ<br />

แอนดรอยด์ตั้งแต่เวอร์ชั่น 1.5 ลงไปก็อาจเกิดความผิดพลาดได้


เครื่องมือดีบั๊กแอพของชุดพัฒนาโปรแกรม Android SDK<br />

การออกแบบโครงสร้างการทำงานของแอพที่มีขนาดของ Hierarchy ไม่ลึกจนเกินไปจะทำให้<br />

แอพทำงานได้รวดเร็ว ซึ่งในกรณีของการสร้างเลย์เอาต์ เราสามารถใช้คำสั่ง RelativeLayout แทน<br />

คำสั่ง LinearLayout เพื่อเพิ่มประสิทธิภาพการทำงานได้<br />

311<br />

รูปที่ 12.7 หน้าจอของเลย์เอาต์ของ Hierarchy Viewer<br />

กรรมวิธี: การใช้งาน TraceView<br />

TraceView เป็นเครื่องมือที่ใช้ปรับประสิทธิภาพการทำงาน เวลาที่ใช้เครื่องมือนี้ เราต้อง<br />

ประกาศการใช้งานคลาส Debug ในแอพของเราก่อนเพื่อให้แอพสร้างข้อมูล Log และข้อมูล trace<br />

ขึ้นมาเพื่อนำไปใช้วิเคราะห์การทำงาน ในหัวข้อนี้จะแสดงวิธีการใช้งาน TraceView ซึ่งจะสร้างเมธอด<br />

เพื่อคำนวณค่าแฟคทอเรียลและให้เมธอดอื่นมาเรียกใช้งานเมธอดนี้ แอคทิวิตี้หลักของการทำงานนี้<br />

แสดงอยู่ในชุดคำสั่งที่ 12.1<br />

ชุดคำสั่งที่ 12.1 src/com/cookbook/android/debug/traceview/testfactorial.java<br />

package com.cookbook.android.debug.traceview;<br />

import android.app.Activity;<br />

import android.os.Bundle;<br />

import android.os.Debug;<br />

public class testfactorial extends Activity {<br />

public final String tag="testfactorial";


312 บทที่ 12 การตรวจสอบการทำางานของแอพ<br />

@Override<br />

public void onCreate(Bundle savedInstanceState) {<br />

super.onCreate(savedInstanceState);<br />

setContentView(R.layout.main);<br />

factorial(10);<br />

}<br />

}<br />

public int factorial(int n) {<br />

Debug.startMethodTracing(tag);<br />

int result=1;<br />

for(int i=1; i adb pull /sdcard/testfactorial.trace<br />

เครื่องมือ TraceView จะอยู่ในโฟลเดอร์ Android SDK ซึ่งเราสามารถนำข้อมูลนี้มาวิเคราะห์ได้<br />

> traceview testfactorial.trace<br />

หลังจากที่สคริปต์ทำงานแล้ว มันจะสร้างหน้าจอสำหรับการวิเคราะห์การทำงานขึ้นมาดังรูปที่12.8<br />

หน้าจอจะแสดงข้อมูลของ Timeline Panel และ Profile Panel โดย Timeline Panel จะ<br />

ปรากฏอยู่ที่ส่วนบนของจอภาพคอยแสดงให้เห็นว่าเธรดและเมธอดมีการเริ่มใช้งานและหยุดทำงานเมื่อ<br />

ใด ส่วน Profile Panel จะอยู่ที่ส่วนล่างสุดของจอภาพ ทำหน้าที่แสดงรายละเอียดการทำงานที่เกิดขึ้น<br />

ภายในเมธอด factorial ถ้าเราเอาเมาส์ไปวางที่ไทม์ไลน์ก็จะเห็นช่วงเวลาที่เริ่มทำการ Trace, ช่วงเวลา<br />

ที่เมธอดเริ่มทำงาน และช่วงเวลาที่หยุดการ Trace<br />

Profile Panel จะแสดงรายละเอียดเกี่ยวกับเวลาที่ใช้ในการทำงานของเมธอด Factorial โดย<br />

พาเนลจะแสดงระยะเวลาเป็นแบบเวลารวมทั้งหมดและเปอร์เซ็นต์ที่ใช้ ซึ่งเปอร์เซ็นต์ที่ใช้นั้นจะแทน<br />

เวลาที่ใช้ไปในการทำงานของเมธอด ส่วนเวลารวมทั้งหมดจะเป็นเวลาที่ใช้เรียกเมธอดและเวลาที่ใช้ใน<br />

การทำงานของเมธอดรวมกัน<br />

ไฟล์ .trace จะประกอบไปด้วยไฟล์ Data และไฟล์ Key ซึ่งไฟล์ data จะใช้ในการเก็บข้อมูล<br />

Trace ส่วนไฟล์ Key จะใช้ในการอ้างอิงถึงเธรดและเมธอด ถ้า TraceView ที่ใช้มีเวอร์ชั่นต่ำกว่า<br />

ข้อมูล Trace เราก็จะต้องรวมไฟล์ Data และไฟล์ key นี้เพื่อให้ได้ไฟล์ Trace เอง


เครื่องมือที่ใช้ในการตรวจสอบการทำางานของแอพบนแอนดรอยด์<br />

313<br />

รูปที่ 12.8 หน้าจอของ TraceView<br />

ยังมีวิธีแสดงข้อมูล Trace แบบกราฟิกอีกหนึ่งวิธี โดยวิธีนี้จะต้องอาศัยการติดตั้งโปรแกรม<br />

Graphviz เพื่อช่วยในการสร้างและแสดงผลข้อมูลเป็นแบบกราฟิกโดยใช้ข้อมูลจาก Android:<br />

dmtracedump<br />

เครื่องมือที่ใช้ในการตรวจสอบการทำงานของแอพบน<br />

แอนดรอยด์<br />

ระบบปฏิบัติการแอนดรอยด์พัฒนามาจากระบบปฏิบัติการลีนุกซ์ ดังนั้นเราสามารถใช้เครื่องมือ<br />

ของลีนุกซ์มาทำงานบนแอนดรอดย์ได้ ยกตัวอย่างเช่น การแสดงรายการของแอพที่ทำงานอยู่ รวมถึง<br />

ปริมาณทรัพยากรที่แอพใช้นั้น เราจะใช้คำสั่ง top โดยคำสั่งนี้จะใช้งานผ่านทางคอมมานด์ไลน์ในขณะ<br />

ที่อุปกรณ์แอนดรอยด์เชื่อมต่อกับคอมพิวเตอร์หรือใช้งานระบบจำลอง<br />

> adb shell top<br />

รูปที่ 12.9 จะแสดงผลลัพธ์ของการใช้คำสั่ง top<br />

คำสั่ง top จะแสดงปริมาณการใช้หน่วยประมวลผลและหน่วยความจำทั้งหมดของระบบด้วย<br />

โดยใช้หน่วยเป็นเปอร์เซ็นต์<br />

คำสั่งอื่นๆ ที่น่าสนใจอย่างเช่น ps จะใช้ในการแสดงรายการของโปรเซสที่กำลังทำงานอยู่ใน<br />

ขณะนั้น<br />

> adb shell ps<br />

ผลลัพธ์ของการใช้คำสั่ง ps จะแสดงอยู่ในรูปที่ 12.10


314 บทที่ 12 การตรวจสอบการทำางานของแอพ<br />

รูปที่ 12.9 ผลลัพธ์ของการใช้คำาสั่ง top<br />

รูปที่ 12.10 ผลลัพธ์ของการใช้คำาสั่ง ps<br />

คำสั่ง ps จะแสดงค่าของโปรเซส ID (PID) และ User ID ของแต่ละโปรเซส ส่วนการใช้งาน<br />

หน่วยความจำจะแสดงผ่านการใช้คำสั่ง dumpsys<br />

> adb shell dumpsys meminfo <br />

ผลลัพธ์ของการใช้คำสั่ง dumpsys จะแสดงอยู่ในรูปที่ 12.11


เครื่องมือดีบั๊กแอพของชุดพัฒนาโปรแกรม Android SDK<br />

315<br />

รูปที่ 12.11 ผลลัพธ์ของการใช้คำาสั่ง dumpsys<br />

คำสั่ง dumpsys จะแสดงรายละเอียดการทำงานของคอมโพเน็นต์จาวาและคอมโพเน็นต์<br />

C/C++ ข้อมูลดังกล่าวจะเป็นประโยชน์ในการปรับแต่งและวิเคราะห์การทำงานของแอพที่พัฒนาด้วย<br />

NDK (Native Development Kit) ซึ่งในรายละเอียดของหน่วยความจำนั้นจะแสดงจำนวนของการ<br />

เรียกใช้วิวในแต่ละโปรเซส รวมถึงจำนวนของแอคทิวิตี้ที่เรียกใช้<br />

กรรมวิธี: การกำหนดค่าเพื่อดีบั๊กด้วย GDB<br />

GDB (GNU Project Debugger) เป็นเครื่องมือที่ใช้ในการดีบั๊กแอพของระบบปฏิบัติการลีนุกซ์<br />

ซึ่งในระบบปฏิบัติการแอนดรอยด์นั้น เราสามารถนำมาใช้กับการดีบั๊กแอพประเภท Native ได้ เช่น<br />

ไลบรารี NDK r4 โดยเครื่องมือนี้จะสร้าง gdbserver และ gdb.setup ขึ้นมา เราสามารถติดตั้ง<br />

gdb โดยใช้คำสั่งดังนี้<br />

> adb shell<br />

> adb /data/<br />

> mkdir myfolder<br />

> exit<br />

> adb push gdbserver /data/myfolder<br />

การใช้งาน gdb จะใช้คำสั่งดังนี้<br />

> adb shell /data/myfolder/gdbserver host:port <br />

ยกตัวอย่างเช่น มีแอพชื่อ myprogram กำลังทำงานอยู่บนอุปกรณ์แอนดรอยด์ ซึ่งมี IP<br />

Address เป็น 10.0.0.1 และใช้งานพอร์ตเบอร์ 1234 เราก็จะใช้คำสั่งด้านล่างนี้เพื่อสั่งให้เซิร์ฟเวอร์<br />

เริ่มทำงาน<br />

> adb shell /data/myfolder/gdbserver 10.0.0.1:1234 myprogram


316 บทที่ 12 การตรวจสอบการทำางานของแอพ<br />

หลังจากนั้นให้เปิดหน้าจอเทอร์มินัล และสั่งให้ gdb ทำงานร่วมกับแอพ myprogram<br />

> gdb myprogram<br />

(gdb) set sysroot ../<br />

(gdb) set solib-search-path ../system/lib<br />

(gdb) target remote localhost:1234<br />

ที่ตำแหน่งเครื่องหมาย prompt ของ gdb คำสั่งแรกจะเป็นการกำหนดให้ทำงานที่รูทไดเร็กทอรี<br />

ส่วนคำสั่งที่ 2 จะเป็นการกำหนดค่า Search Path ของ Shared Libraries และคำสั่งสุดท้ายจะ<br />

เป็นการกำหนดเป้าหมายที่จะดีบั๊ก หลังจากที่เครื่องเป้าหมายทำการรันแอพ myprogram แล้ว<br />

แอพก็จะเริ่มทำการดีบั๊กแอพทันที


317<br />

เล่มละ<br />

350<br />

ขายดี<br />

ที่สุด!<br />

The Hot Shoe Diaries 1-2<br />

บันทึกลับถ่ายภาพสวยด้วยแฟลช<br />

ในแบบฉบับ Joe McNally<br />

หนังสือที่เผยกรรมวิธีในการเนรมิตแสงจากแฟลชกล้อง<br />

ธรรมดาๆ ให้กลายเป็นแสงสวยสุดอลังการ รวมทั้งบอกเล่า<br />

แนวคิดในการสร้างสรรค์ภาพถ่ายให้สวยแปลกตาไม่ซ้ำใคร<br />

ในสไตล์ Joe McNally ที่ช่างภาพทั่วโลกให้การยอมรับ<br />

Getting Started in Digital<br />

SLR Photography<br />

รู้ลัด ทันใจ มือใหม่หัวใจโปรกล้อง DSLR<br />

หนังสือที่อัดแน่นไปด้วยข้อมูล คำแนะนำ และหลากหลาย<br />

เทคนิคที่จะช่วยพัฒนาฝีมือของช่างภาพให้ดียิ่งขึ้น ซึ่งผ่าน<br />

การพิสูจน์แล้วว่าได้ผลจริง รวมไปถึงการให้คำแนะนำดีๆ<br />

ในการเลือกซื้ออุปกรณ์ถ่ายภาพที่ดีที่สุดจากมืออาชีพด้วย<br />

HOT!<br />

ราคา<br />

350<br />

หาซื้อได้ตามร้านหนังสือชั้นนำทั่วประเทศ<br />

หรือติดต่อที่ บริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำกัด โทร. 0-2642-2733-36 ต่อ 709


The Adobe Photoshop Lightroom 3 Book<br />

คัมภีร์ Lightroom 3 ฉบับสมบูรณ์<br />

คู่มือที่จะแนะนำกลเม็ดเด็ด<br />

สำหรับจัดการและตกแต่ง<br />

ภาพถ่ายดิจิตอลด้วยวิธีง่ายๆ<br />

ในเวลาอันสั้น แต่ได้คุณภาพสูงสุด<br />

ระดับมืออาชีพ<br />

ราคา<br />

495.-<br />

คู่มือที่ดีที่สุด ละเอียดที่สุด และขายดีที่สุด!<br />

The Adobe Photoshop CS5 Book<br />

คัมภีร์ตกแต่งภาพดิจิตอลระดับมืออาชีพ<br />

สารพัดเทคนิคปรับแต่งแก้ไข<br />

ภาพถ่ายดิจิตอลให้สวยเฉียบแบบ<br />

มืออาชีพด้วย Photoshop CS5<br />

จาก Scott Kelby<br />

ราคา<br />

495.-<br />

หาซื้อได้ตามร้านหนังสือชั้นนำทั่วประเทศ<br />

หรือติดต่อที่ บริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำกัด โทร. 0-2642-2733-36 ต่อ 709


​The​​Digital​​Photography​ ​Book​ ​Vol​.​1​, 2, 3<br />

​เคล็ด​(​ไม่​)​ลับ​ ​ถ่าย​ภาพ​สวย​ด้วย​กล้อง​ดิจิตอล​​เล่ม​​1, 2​, 3<br />

ไม่​สำคัญ​ว่า​กล้อง​เล็ก​<br />

​กล้อง​ใหญ่​คุณ​ก็​ถ่าย​ภาพ<br />

​ให้​สวย​เฉียบ​ได้​<br />

ใน​ทุก​สภาวะ​<br />

ด้วย​เทคนิค​สุด​เจ๋ง​<br />

จาก​เซียน​กล้อง​<br />

Scott​​Kelby​<br />

ราคา<br />

250.-<br />

Photo Recipes Live: Behind The Scenes<br />

เคล็ด(ไม่)ลับ การจัดแสง ถ่ายภาพสวย<br />

เรียนรู้เทคนิคเด็ดๆ ในการถ่ายภาพสวยด้วยแสงอันหลากหลาย<br />

ในสถานการณ์ต่างๆ เช่น การถ่ายภาพคู่รัก,<br />

การจัดแสงถ่ายภาพแนวสวยใส / คมเข้ม,<br />

การถ่ายภาพบุคคลโดยใช้แสงธรรมชาติ<br />

สาธิตให้เห็นกันสดๆ<br />

โดย Scott Kelby ผ่านไฟล์วิดีโอ<br />

พร้อมซับไทย จุใจ 2 ชั่วโมงเต็ม!<br />

ราคา<br />

250.-<br />

หาซื้อได้ตามร้านหนังสือชั้นนำทั่วประเทศ<br />

หรือติดต่อที่ บริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำกัด โทร. 0-2642-2733-36 ต่อ 709


วรรณกรรมเยาวชนชั้นเยี่ยม<br />

ที่ได้รับความนิยมสูงสุด<br />

จนถูกสร้างเป็นภาพยนตร์ดังของฮอลลีวู้ด<br />

เรื่องราวของเหล่านกฮูกที่เต็มไปด้วยการผจญภัย<br />

และการต่อสู้อันยิ่งใหญ่เพื่อปกป้องและกอบกู้อาณาจักรของนกฮูกกลับคืนมา<br />

เล่ม 1 ตอน กำเนิดผู้กล้า เล่ม 2 ตอน โบยบินด้วยแรงศรัทธา<br />

ราคา 200 บาท<br />

ราคา 220 บาท<br />

เล่ม 3 ตอน บุกปราสาทมาร<br />

ราคา 220 บาท<br />

เล่ม 4 ตอน ชีพนี้พลีเพื่อกาฮูล<br />

ราคา 220 บาท


วรรณกรรมเยาวชนดีๆ<br />

ที่อยากแนะนำาจากสำานักพิมพ์<br />

การันตีด้วยรางวัลระดับโลก Newbery Honor Book<br />

หนูน้อยหมิ่นลี่<br />

กับปริศนาผู้เฒ่าจันทรา<br />

ราคา 240 บาท<br />

เรื่องราวการเดินทางอันน่าอัศจรรย์<br />

ของสาวน้อยหมิ่นลี่ที่ออกตามหาผู้เฒ่าจันทรา<br />

เพื่อให้พบกับคำาตอบของชีวิต<br />

ราคา 290 บาท<br />

หรือติดต่อที่ บริษัท ทรู ดิจิตอล คอนเท้นท์ แอนด์ มีเดีย จำากัด<br />

โทร. 0-2642-2733-36 ต่อ 709<br />

สำนักพิมพ์ Papyrus by TrueLife<br />

TRUE DIGITAL CONTENT AND MEDIA COMPANY LIMITED<br />

121/102-103 อาคารอาร์เอสทาวเวอร์ ชั้น 38<br />

ถนนรัชดาภิเษก แขวงดินแดง เขตดินแดง กรุงเทพฯ 10400<br />

โทรศัพท์ 0-2642-2733-36 โทรสาร 0-2642-2739 อีเมล์แอดเดรส : contact@truebookstore.com<br />

บันทึกโลกใบเล็กของ<br />

แคลเปอร์เนีย<br />

ร่วมลุ้นไปกับการผจญภัย<br />

ของสาวน้อยแคลเปอร์เนียและคุณปู่คู่หูต่างวัย<br />

ที่จะพาไปตะลุยในโลกแห่งวิทยาศาสตร์<br />

พบกันได้แล้ววันนี้ที่ร้าน SE-ED BOOK CENTER<br />

และร้านหนังสือชั้นนำาทั่วสยามประเทศ


เมื่อเราสามารถย่อชั้นหนังสือ<br />

มาอยู่ในมือคุณได้<br />

พบกับคลังหนังสือดิจิตอลของ True Digital Bookstore<br />

ที่จะเข้าไปเป็นส่วนหนึ่งของ www.truelife.com ที่มียอด<br />

สมาชิกมากกว่า 1,800,000 คน อย่างเป็นทางการ รวมทั้ง<br />

เพิ่ม Section ใหม่ eMagazine, eNewspaper และ eComic<br />

เพื่อรองรับไลฟ์สไตล์การอ่านที่หลากหลายมากขึ้นโดยมาพร้อม<br />

กับระบบสมัครสมาชิกนิตยสาร และแพ็คเกจใหม่ที่พัฒนา<br />

ขึ้นมาล่าสุด เพื่อให้นักอ่านสะดวกสบายยิ่งขึ้น<br />

คลังหนังสือดิจิตอล ตอบโจทย์สุดล้ำกับระบบป้องกันการละเมิดลิขสิทธิ์ พร้อมเสิร์ฟคอนเท้นท์หลากหลายจากสำนักพิมพ์ชั้นนำ<br />

สมัครสมาชิกวันนี้ ดาวน์โหลดฟรีหนังสือดิจิตอลชื่อดังกว่า 100 รายการ<br />

ที่ book.truelife.com และ www.truebookstore.com เพิ่มเติมโทร 02-642-2733

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!