19 October 2013

จัดการกับเรื่องการหมุนหน้าจอด้วย Orientation Manager

Updated on

        พอดีเจ้าของบล็อกต้องเขียนแอปพลิเคชันที่รองรับการหมุนจอ แต่ทีนี้บนแอนดรอยด์นั้นมีปัญหาเรื่อง Fragment อย่างหนึ่ง คือเรื่องความต่างระหว่าง Phone, Tablet 7" และ Tablet 10" ก็เลยเขียนคลาสสำหรับจัดการกับเรื่องการหมุนหน้าจอเล่นๆ

        ก่อนอื่นคงต้องขอพูดเรื่อง Orientation ของอุปกรณ์แอนดรอยด์กันก่อน
        Orientation หรือทิศทางของตัวเครื่อง เพราะว่า Smart Device เหล่านี้ สามารถใช้งานโดยหมุนทิศทางของจอได้ตามการใช้งาน ดังนั้นจึงต้องมีการรู้ว่า Orientation ของเครื่อง ณ ตอนนั้นอยู่ทิศทางใด

        สำหรับอุปกรณ์แอนดรอยด์นั้นจะมีทิศทางหลักหรือ Natural Orientation ซึ่งเป็นทิศทางการใช้งานหลักของเครื่องแต่ละแบบดังนี้


        สำหรับ Tablet 10 นิ้ว ถูกสร้างขึ้นมาเพื่อใช้งานในแนวนอนเป็นหลัก เน้นไปที่การใช้งานด้วยการถือสองมือซะมากกว่า ในขณะที่ Tablet 7 นิ้วกับ Phone เน้นการใช้านที่แนวตั้งเป็นหลัก โดยถูกออกแบบมาให้ใช้งานด้วยการถือด้วยมือเดียวได้

        ทีนี้ก็ต่อกันที่ว่าทิศทางของมือถือมีแบบไหนบ้าง

 

        สำหรับคลาส Orientation Manager จะช่วยบอกให้ได้ว่าตอนนี้เครื่องหมุนอยู่ในทิศทางใด และมี Listener คอยเช็คเวลาเครื่องหมุน และที่สำคัญเลยก็คือการหมุนแบบ Mirror หรือทิศทางตรงข้าม 180 องศา โดยปกติการหมุนหน้าจอหนึ่งครั้ง onResume จะถูกเรียกใช้งานใหม่ทุกครั้ง ทำให้สามารถรู้ได้ว่าหน้าจอถูกหมุนเพื่อเปลี่ยนทิศทางอยู่ แต่ในกรณีที่หมุนไปทิศทางตรงข้าม 180 องศาเลย สมมติว่าหมุนจาก Normal Landscape เป็น Reverse Landscape โดย onResume จะไม่ถูกเรียกใหม่อีกครั้ง หรือ onConfigurationChanged ก็เช่นกัน

        ในการทำงานของแอนดรอยด์จะรับรู้จากการเปลี่ยนแปลงของ Layout เท่านั้น อย่างเช่นจาก Normal Landscape ไปเป็น Normal Landscape Layout จะมีการเปลี่ยนแปลงคือจาก Layout แบบแนวตั้งก็เปลี่ยนเป็นแนวนอน ทำให้ onResume และ onConfigurationChanged ถูกเรียกให้ทำงานใหม่ แต่สำหรับการหมุนแบบ Mirror นั้น Layout จะไม่มีการเปลี่ยนแปลง แค่หมุน 180 องศาเท่านั้น แต่อื่นๆที่อยู่ใน Layout ยังคงเดิม จึงทำให้ onResume และ onConfigurationChanged ไม่ถูกเรียกใช้งานนั่นเอง

        สรุปคือ onResume ไม่ได้ทำงานเมื่อมีการหมุนหน้าจอเสมอไป แต่ขึ้นอยู่กับการเปลี่ยนแปลงของ Layout บนหน้าแอปนั้นๆ

        ดังนั้นเจ้าของบล็อกจึงต้องใช้ Orientation Listener เพื่อเช็คมุมการหมุนเอง แล้วสร้างเป็น Orientation Manager Listener เพื่อบอกว่าหน้าจอหมุนแล้ว โดยเขียนให้รองรับการหมุนแบบ Mirror ด้วย จึงหมดปัญหาเรื่องนี้ไปเลย อีกทั้งยังเช็คได้ทันทีว่าทิศทางของเครื่องอยู่ในทิศทางใด และมีคำสั่งในการล็อคหน้าจอไม่ให้หมุนไปมาด้วย เพราะในบางครั้งผู้ที่หลงเข้ามาอ่านอยากให้มีปุ่มล็อคหน้าจอ แบบแอปที่ล็อคให้หยุดหมุนจอได้ และปลดล็อคเพื่อให้หมุนต่อได้

        คลาสนี้เจ้าของบล็อกตั้งชื่อมันว่า OrientationManager มีโค๊ดดังนี้
package app.akexorcist.orientationmanager; import java.lang.reflect.Method; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.pm.ActivityInfo; import android.hardware.SensorManager; import android.os.Build; import android.util.DisplayMetrics; import android.view.Display; import android.view.OrientationEventListener; import android.view.Surface; import android.view.WindowManager; public class OrientationManager { public final static int PORTRAIT_NORMAL = 0; public final static int PORTRAIT_REVERSE = 1; public final static int LANDSCAPE_NORMAL = 2; public final static int LANDSCAPE_REVERSE = 3; Context context; Activity activity; String device_orientation = ""; private OrientationEventListener mOrientationEventListener; private OrientationManagerListener mOrientationManagerListener; @SuppressWarnings("deprecation") @SuppressLint("NewApi") public OrientationManager(Activity activity) { this.activity = activity; this.context = activity.getApplicationContext(); int xres = 0, yres = 0; Method mGetRawH; Display display = activity.getWindowManager().getDefaultDisplay(); DisplayMetrics dm = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(dm); if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { try { mGetRawH = Display.class.getMethod("getRawHeight"); Method mGetRawW = Display.class.getMethod("getRawWidth"); xres = (Integer) mGetRawW.invoke(display); yres = (Integer) mGetRawH.invoke(display); } catch (Exception e) { xres = display.getWidth(); yres = display.getHeight(); } } else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { DisplayMetrics outMetrics = new DisplayMetrics (); display.getRealMetrics(outMetrics); xres = outMetrics.widthPixels; yres = outMetrics.heightPixels; } int hdp = (int)(yres * (1f / dm.density)); int wdp = (int)(xres * (1f / dm.density)); int sw = (hdp < wdp) ? hdp : wdp; device_orientation = (sw >= 720) ? "landscape" : "portrait"; mOrientationEventListener = new OrientationEventListener(context , SensorManager.SENSOR_DELAY_NORMAL){ int orientation = -1; public void onOrientationChanged(int arg0) { if(orientation == -1) { orientation = getOrientation(); } else { if(orientation != getOrientation()) { if((orientation == PORTRAIT_NORMAL && getOrientation() == PORTRAIT_REVERSE) || (orientation == PORTRAIT_REVERSE && getOrientation() == PORTRAIT_NORMAL) || (orientation == LANDSCAPE_NORMAL && getOrientation() == LANDSCAPE_REVERSE) || (orientation == LANDSCAPE_REVERSE && getOrientation() == LANDSCAPE_NORMAL)) { mOrientationManagerListener.onMirrorRotatation( getOrientation()); mOrientationManagerListener.onOrientationChanged( getOrientation(), true); } else { mOrientationManagerListener.onOrientationChanged( getOrientation(), false); } orientation = getOrientation(); } } } }; } public void setOnOrientationListener (OrientationManagerListener listener) { mOrientationManagerListener = listener; } public void enable() { mOrientationEventListener.enable(); } public void disable() { mOrientationEventListener.disable(); } @SuppressWarnings("static-access") public int getOrientation() { WindowManager wm = (WindowManager)context.getSystemService( context.WINDOW_SERVICE); int rotation = wm.getDefaultDisplay().getRotation(); if(device_orientation.equals("portrait")) { if(rotation == Surface.ROTATION_0) { return PORTRAIT_NORMAL; } else if(rotation == Surface.ROTATION_90) { return LANDSCAPE_NORMAL; } else if(rotation == Surface.ROTATION_180) { return PORTRAIT_REVERSE; } else if(rotation == Surface.ROTATION_270) { return LANDSCAPE_REVERSE; } } else if(device_orientation.equals("landscape")) { if(rotation == Surface.ROTATION_0) { return LANDSCAPE_NORMAL; } else if(rotation == Surface.ROTATION_90) { return PORTRAIT_REVERSE; } else if(rotation == Surface.ROTATION_180) { return LANDSCAPE_REVERSE; } else if(rotation == Surface.ROTATION_270) { return PORTRAIT_NORMAL; } } return -1; } public void disableRotation() { disable(); int SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8; int SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9; if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.FROYO){ SCREEN_ORIENTATION_REVERSE_LANDSCAPE = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; SCREEN_ORIENTATION_REVERSE_PORTRAIT = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } int rotation = getOrientation(); switch(rotation) { case OrientationManager.PORTRAIT_NORMAL: activity.setRequestedOrientation( ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); break; case OrientationManager.PORTRAIT_REVERSE: activity.setRequestedOrientation( SCREEN_ORIENTATION_REVERSE_PORTRAIT); break; case OrientationManager.LANDSCAPE_NORMAL: activity.setRequestedOrientation( ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); break; case OrientationManager.LANDSCAPE_REVERSE: activity.setRequestedOrientation( SCREEN_ORIENTATION_REVERSE_LANDSCAPE); break; } } public void enableRotation() { enable(); activity.setRequestedOrientation( ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } public interface OrientationManagerListener{ public void onOrientationChanged(int orientation, boolean isMirror); public void onMirrorRotatation(int orientation); } }

        ไม่ต้องไปซีเรียสมากกับโค๊ดอันนี้นะ เจ้าของบล็อกเขียนขึ้นมาให้ใช้งานเฉยๆ

        สำหรับการเรียกใช้งานคลาส OrientationManager จะเป็นดังนี้
Orientation om = new OrientationManager(ActiviyName.this);

        สมมติว่าเจ้าของบล็อกตั้งชื่อออบเจ็คว่า om ละกันนะ ให้ดูตรงที่ ActivityName ให้ใส่ชื่อ Activity ที่เรียกใช้คลาสนี้ เช่น เจ้าของบล็อกเรียกใช้คลาสนี้ใน Main.java ก็จะเป็น
Orientation om = new OrientationManager(Main.this);

        โดยจะมีคำสั่งทั้งหมดดังนี้
om.getOrientation(); om.enable(); om.disable(); om.enableRotation(); om.disableRotation(); om.setOnOrientationListener(Listener);


getOrientaion
        เป็นคำสั่งสำหรับรับค่าทิศทางของตัวเครื่องนั้น โดยค่าที่ Return กลับมาจะเป็นค่า Integer แทนทิศทางดังนี้
public final static int PORTRAIT_NORMAL = 0; public final static int PORTRAIT_REVERSE = 1; public final static int LANDSCAPE_NORMAL = 2; public final static int LANDSCAPE_REVERSE = 3;


enable
        เริ่มทำการเช็คทิศทางของเครื่องเพื่อตรวจจับการเปลี่ยนแปลงใช้ในกรณีที่เรียกใช้ OrientationManagerListener เท่านั้น โดยเรียกใช้คำสั่งนี้ใน onResume


disable
        หยุดทำการเช็คทิศทางของเครื่องเพื่อตรวจจับการเปลี่ยนแปลงใช้ในกรณีที่เรียกใช้ OrientationManagerListener เท่านั้น โดยเรียกใช้คำสั่งนี้ใน onPause


enableRotation
        กำหนดให้ Activity นั้นๆสามารถหมุนทิศทางจอได้


disableRotation
        กำหนดให้ Activity นั้นๆไม่สามารถหมุนทิศทางจอได้


setOnOrientationListener
        สร้าง Listener สำหรับตรวจจับเมื่อเครื่องมีการเปลี่ยนแปลงทิศทางจอ โดยมีฟังก์ชันสองฟังก์ชันคือ onOrientationChanged กับ onMirrorRotation
om.setOnOrientationListener(new OrientationManagerListener() { public void onOrientationChanged(int orientation, boolean isMirror) { // เมื่อมีการหมุนหน้าจอเปลี่ยนไปในทิศทางใดๆ } public void onMirrorRotatation(int orientation) { // เมื่อมีการหมุนหน้าจอในทิศทางตรงกันข้าม } });

        สำหรับ onOrientationChanged จะทำงานทุกครั้งที่หน้าจอเปลี่ยนแปลงทิศทาง แม้แต่การหมุนแบบทิศทางตรงกันข้ามอย่าง Mirror ก็ด้วย โดยจะมีพารามิเตอร์ส่งเข้ามาในฟังก์ชันนี้ด้วยกันสองตัว
public void onOrientationChanged(int orientation, boolean isMirror) { // orientation : ทิศทางของหน้าจอ (ค่าเดียวกับคำสั่ง getOrientation) // isMirror : เป็นการหมุนแบบทิศทางตรงกันข้ามหรือไม่ (true ใช่, false ไม่ใช่) }

        ส่วน onMirrorRotation เอาไว้สำหรับกรณีที่หมุนในทิศทางตรงกันข้ามโดยเฉพาะ
public void onMirrorRotatation(int orientation) { // orientation : ทิศทางของหน้าจอ (ค่าเดียวกับคำสั่ง getOrientation) }

*- เพิ่มเติมสำหรับมือใหม่ -*
        จะรู้ได้ไงว่าค่าทิศทางจากตัวแปร orientation เป็นทิศทางไหน ก็ใช้ IF หรือ Switch-Case ในการเช็คว่าค่าตรงกับทิศทางไหน
if(orientation == OrientationManager.PORTRAIT_NORMAL) { // หน้าจอแสดงทิศทาง Normal Portrait } else if(orientation == OrientationManager.PORTRAIT_REVERSE) { // หน้าจอแสดงทิศทาง Reverse Portrait } else if(orientation == OrientationManager.LANDSCAPE_NORMAL) { // หน้าจอแสดงทิศทาง Normal Landscape } else if(orientation == OrientationManager.LANDSCAPE_REVERSE) { // หน้าจอแสดงทิศทาง Reverse Landscape }

        เท่านี้ก็รู้ได้แล้วว่าเป็นทิศทางใดอยู่

        ทีนี้มาดูตัวอย่างการใช้งานกันเลยดีกว่า โดยเจ้าของบล็อกจะให้แสดงบนหน้าจอว่า ตอนนี้เครื่องกำลังอยู่ในทิศทางแบบใดอยู่ โดยแสดงบอกว่า Text View ที่อยู่กลางหน้าจอและมุมขวาบนจะมีปุ่ม Toggle Button เอาไว้เปิด-ปิดการล็อคหน้าจอ ถ้า On จะเป็นการล็อค


main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" > <TextView android:id="@+id/textView1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="@string/hello_world" /> <ToggleButton android:id="@+id/toggleButton1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:text="ToggleButton" /> </RelativeLayout>


        สำหรับโค๊ดก็ประมาณนี้


Main.java
package app.akexorcist.orientationmanager; import android.os.Bundle; import android.app.Activity; import android.util.Log; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.TextView; import android.widget.ToggleButton; import app.akexorcist.orientationmanager.OrientationManager.OrientationManagerListener; public class Main extends Activity { OrientationManager om; TextView textView1; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); textView1 = (TextView)findViewById(R.id.textView1); ToggleButton toggleButton1 = (ToggleButton)findViewById(R.id.toggleButton1); toggleButton1.setOnCheckedChangeListener( new OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView , boolean isChecked) { if(isChecked) om.disableRotation(); else om.enableRotation(); } }); om = new OrientationManager(Main.this); om.setOnOrientationListener(new OrientationManagerListener() { public void onOrientationChanged(int orientation , boolean isMirror) { if(!isMirror) { Log.i("OrientationManager", "Orientation Changed"); checkOrientation(orientation); } } public void onMirrorRotatation(int orientation) { Log.i("OrientationManager", "Mirror Rotatation"); checkOrientation(orientation); } }); checkOrientation(om.getOrientation()); } public void onResume() { super.onResume(); om.enable(); } public void onPause() { super.onPause(); om.disable(); } public void checkOrientation(int orientation) { if(orientation == OrientationManager.PORTRAIT_NORMAL) textView1.setText("Normal Portrait"); else if(orientation == OrientationManager.PORTRAIT_REVERSE) textView1.setText("Reverse Portrait"); else if(orientation == OrientationManager.LANDSCAPE_NORMAL) textView1.setText("Normal Landscape"); else if(orientation == OrientationManager.LANDSCAPE_REVERSE) textView1.setText("Reverse Landscape"); } }

        1. ประกาศเรียกใช้คลาส OrientationManager และ Text View

        2. กำหนดค่าให้กับ Text View เพื่อใช้แสดงทิศทางเครื่อง

        3. ประกาศและกำหนดค่าให้กับ Toggle Button และเรียกใช้ Listener แบบ onCheckChangedListener ที่จะทำงานเมื่อ Toggle Button เปลี่ยนจากสถานะ On เป็น Off หรือจาก Off เป็น On ถ้าสถานะเป็น On ให้ล็อคการหมุนจอด้วยคำสั่ง disableRotation และถ้าเป็น Off ให้ปลดล็อคการหมุนจอด้วยคำสั่ง enableRotation

        4. กำหนดค่าให้กับ OrientationManager และเรียกใช้ Listener แบบ onOrientationManager ที่จะทำงานเมื่อมีการหมุนหน้าจอ

        5. เมื่อมีการหมุนหน้าจอจะทำการเช็คก่อนว่าหมุนแบบ 180 องศาหรือไม่ โดยเช็คจากตัวแปร isMirror ว่าเป็น True หรือ False ถ้าเป็น False ก็คือหมุนแค่ 90 องศาปกติ จะให้แสดงข้อความบน Log และเรียกฟังก์ชัน checkOrientation เพื่อแสดงบน Text View ว่าหมุนทิศทางใด

        6. เมื่อมีการหมุนหน้าจอแบบ 180 องศา จะให้แสดงข้อความบน Log และเรียกฟังก์ชัน checkOrientation เพื่อแสดงบน Text View ว่าหมุนทิศทางใด

        7. เรียกฟังก์ชัน checkOrientation เพื่อแสดงบน Text View ว่าหมุนทิศทางใด โดยเรียกตั้งแต่แรกเริ่มใน onCreate เพื่อให้เปิดแอปขึ้นมาแล้วแสดงค่าทันที

        8. ฟังก์ชัน onResume เรียกคำสั่ง enable เพื่อเช็คทิศทางการหมุนจอ สำหรับ OrientationManagerListener

        9. ฟังก์ชัน onPause เรียกคำสั่ง disable เพื่อหยุดเช็คทิศทางการหมุนจอ สำหรับ OrientationManagerListener

        10. ฟังก์ชันที่สร้างขึ้นมาเพื่อเช็คค่าจากตัวแปร orientation ว่าตอนนั้นๆหน้าจอกำลังหมุนอยู่ในทิศทางใด โดยเปรียบเทียบกับค่าในคลาส OrientationManager แล้วแสดงทิศทางที่ได้บน Text View

        ** ในกรณีที่ไม่ใช้ OrientationManagerListener ก็เอาคำสั่ง enable กับ disable ออกได้เลย **


AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.akexorcist.orientationmanager" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name="app.akexorcist.orientationmanager.Main" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

        เมื่อลองทดสอบแอปก็ให้หมุนหน้าจอดู ก็จะเห็นข้อความกลางจอเปลี่ยนข้อความไปตามทิศทางของหน้าจอ ถ้าหมุนหน้าจอไม่ได้ให้เช็คว่าเครื่องได้เปิด Auto Rotate Screen ใน Settings แล้วหรือยัง และเมื่อลองกดปุ่ม Toggle Button เพื่อล็อคทิศทางหน้าจอ เวลาหมุนหน้าจอไปในทิศทางอื่นก็จะไม่มีผล

        สำหรับผู้ที่หลงเข้ามาอ่านคนใดต้องการไฟล์ตัวอย่างสามารถดาวน์โหลดได้ที่ Orientation Manager [Google Drive]