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 />
ใส่ตัวอักษรพิเศษได้ เช่น ©<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
TheDigitalPhotography Book Vol.1, 2, 3<br />
เคล็ด(ไม่)ลับ ถ่ายภาพสวยด้วยกล้องดิจิตอลเล่ม1, 2, 3<br />
ไม่สำคัญว่ากล้องเล็ก<br />
กล้องใหญ่คุณก็ถ่ายภาพ<br />
ให้สวยเฉียบได้<br />
ในทุกสภาวะ<br />
ด้วยเทคนิคสุดเจ๋ง<br />
จากเซียนกล้อง<br />
ScottKelby<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