บทความที่แล้วทำเรื่อง Object Animator ไป วันนี้ก็ขอเอามาประยุกต์ใช้งานเล่นๆดูบ้าง (ซึ่งจริงๆแล้วเจ้าของบล็อกจะทำบทความนี้อยู่แล้ว แต่ทว่าต้องทำบทความเกริ่นเสียก่อน) ถ้าผู้ที่หลงเข้ามาอ่านใช้ Facebook และ Google+ อยู่บ่อยๆ ถ้าลองสังเกตดีๆก็จะเห็นว่าแถบปุ่มเมนูของ Facebook ที่อยู่ข้างล่าง และแถบเมนูทั้งหมดของ Google+ จะมีการซ่อนเมื่อผู้ใช้เลื่อนดู Feed ต่างๆ
เจ้าของบล็อกก็เลยเกิดความรู้สึกว่าอยากลองทำดูบ้าง เพราะไปพบว่าแท้ที่จริงแล้วมันก็ไม่ได้ยากซักเท่าไรนัก และที่สำคัญคือลูกเล่นแบบนี้ควรมีไว้ก็ดีเพราะมันทำให้แอปพลิเคชันดูน่าสนใจยิ่งขึ้น
อะไรนะ! ไม่ได้สังเกตเรอะ!? ว่าเมนูมันซ่อนได้น่ะ?
ว่าแล้วก็แปะภาพให้ดูกันก่อนเลย
การที่ผู้ใช้เลื่อนลงนั้นเพราะว่าต้องการอ่าน Content ไปเรื่อยๆ จึงทำให้แถบเมนูนั้นบัง Content ไปโดยปริยาย จะดีกว่ามั้ยถ้าซ่อนมันซะ เพื่อให้มีพื้นที่อ่านได้มากขึ้น ในขณะเดียวกันเมื่อผู้ใช้เลื่อนขึ้นมักจะเป็นการต้องการทำอะไรซักอย่าง เพราะว่าอ่าน Content เสร็จแล้ว ดังนั้นแถบเมนูจึงถูกเรียกขึ้นในเวลานี้เพื่อให้ผู้ใช้กดแถบเมนูเพื่อทำอย่างอื่นต่อไป
ดังนั้นคอนเซปท์นี้ก็คือ
ทำไมแถบเมนูจะต้องแสดงตลอดเวลา ในเมื่อมีบางช่วงเวลาที่ผู้ใช้ไม่ต้องการกดแถบเมนู
ซึ่งพฤติกรรมการเลื่อน Feed ขึ้นลงนี่ล่ะที่จะบอกได้ประมาณนึงว่าผู้ใช้ทำอะไร
แล้วมันทำยังไงล่ะ?
ก่อนจะเสพย์ข้อมูลจากเนื้อหาต่อไปนี้ เจ้าของบล็อกก็อยากให้ผู้ที่หลงเข้ามาอ่านลองคิดตามดูก่อนบ้างว่า ถ้าผู้ที่หลงเข้ามาอ่านทำเอง จะทำอย่างไรถึงจะได้แบบนั้น
ก่อนจะเสพย์ข้อมูลจากเนื้อหาต่อไปนี้ เจ้าของบล็อกก็อยากให้ผู้ที่หลงเข้ามาอ่านลองคิดตามดูก่อนบ้างว่า ถ้าผู้ที่หลงเข้ามาอ่านทำเอง จะทำอย่างไรถึงจะได้แบบนั้น
สำหรับวิธีที่เจ้าของบล็อกใช้ก็คือจับระยะทางในการ Scroll ผ่าน OnTouchListener ว่าเลื่อนนิ้วไปเป็นระยะเท่าไรแล้ว ถ้าเลื่อนนิ้วลงก็จะให้แสดงเมนู และถ้าเลื่อนนิ้วขึ้นก็จะเป็นการซ่อนเมนู
อย่าสับสนกันนะเออ
สมมติว่าเจ้าของบล็อกสร้าง Layout ขึ้นมาดังนี้
โดยสองแถบบนคือเมนูและแถบล่างก็คือเมนู ซึ่งแถบบนสุดปกติจะเป็น Action Bar แต่เจ้าของบล็อกสร้าง Layout เลียนแบบ Action Bar แทน แล้วซ่อนของจริงไว้ ซึ่งทำได้ทั้งคู่น่ะแหละ ถ้าใช้ Action Bar ก็แค่ใช้คำสั่งซ่อนและแสดงเท่านั้นเอง
ก่อนอื่นขอตั้งชื่อแถบเมนูแต่ละแถบเสียก่อน เพื่อที่เวลาอธิบายจะได้ไม่สับสน
หัวใจสำคัญอย่างหนึ่งของการทำลูกเล่นแบบนี้ก็คือ Parent ควรจะเป็น Relative Layout เพื่อให้ Content ซ้อนอยู่ข้างหลังแบบเต็มหน้าจอ เพราะเวลาซ่อนแถบเมนูไปแล้วจะทำให้เห็นแบบเต็มจอทันที ถ้าให้ย่อ-ขยายตามการซ่อนของเมนูมันจะทำให้เห็นการขยายตัวของ Content ทำให้รู้สึกไม่ต่อเนื่อง
สำหรับการจัดรายละเอียดปลีกย่อยในแถบเมนูต่างๆก็แล้วแต่ความต้องการเลย เพราะส่วนสำคัญจะอยู่ที่การสั่งที่ Layout ของแถบเมนูต่างๆเลย โดยในกรณีนี้เจ้าของบล็อกได้กำหนดความสูงของแถบเมนูทุกอันไว้ที่ 48dp (เป็นความสูงที่เหมาะสมสำหรับปุ่มใดๆที่แสดงบนอุปกรณ์แอนดรอยด์ที่เป็น Phone)
activity_main.xml
activity_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" >
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FFFFFF"
android:scrollbars="none" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical" >
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginBottom="30dp"
android:layout_marginLeft="30dp"
android:layout_marginRight="30dp"
android:layout_marginTop="100dp"
android:src="@drawable/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:text="@string/sample_text"
android:textSize="16sp" />
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_margin="30dp"
android:src="@drawable/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:text="@string/sample_text"
android:textSize="16sp" />
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_margin="30dp"
android:src="@drawable/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="50dp"
android:layout_marginLeft="50dp"
android:layout_marginRight="50dp"
android:text="@string/sample_text"
android:textSize="16sp" />
</LinearLayout>
</ScrollView>
<RelativeLayout
android:id="@+id/layoutMenu"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:background="#d3415d"
android:gravity="right" >
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layoutHeader"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_below="@+id/layoutActionBar"
android:background="#ebebeb" >
<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_margin="4dp"
android:layout_toRightOf="@+id/button3"
android:text="Header"
android:textSize="15sp" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layoutActionBar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#d3415d" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:src="@drawable/ic_launcher" />
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_toRightOf="@+id/imageView1"
android:text="Just Scroll It!"
android:textColor="#FFFFFF"
android:textSize="18sp" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button" />
</RelativeLayout>
</RelativeLayout>
หมายเหตุ - @string/sample_text เป็นข้อความที่เจ้าของบล็อกเก็บไว้ใน res/values/string.xml เพราะว่าอยากจะใส่ข้อความยาวๆเพื่อให้ Content ล้นหน้าจอจะได้เลื่อนขึ้นลงได้ หรือก็คือ ใส่ข้อความอะไรก็ได้ยาวๆลงไปนั่นแหละ
ID สำคัญๆที่จะใช้ในโค๊ดก็จะมี scrollView ที่เอาไว้แสดง Content (จริงๆสามารถใช้ List View หรือ Adapter View ตัวอื่นๆได้เช่นกัน แค่ไม่อยากให้โค๊ดยาวก็เท่านั้น), layoutActionBar ที่แสดงเป็นแถบ Action Bar, layoutHeader ที่แสดงเป็นแถบ Header และ layoutMenu ที่แสดงเป็นแถบ Menu
สรุป ID ที่จะใช้ scrollView, layoutActionBar, layoutHeader และ layoutMenu
แล้ว Object Animator จะสั่งงานยังไงให้มันซ่อนเมนูได้ล่ะ?
คำถามนี้ทำเผื่อสำหรับผู้ที่หลงเข้ามาอ่านยังมองไม่ออกว่ามันจะทำได้ยังไง เจ้าของบล็อกก็จะให้ Object Animator ไปสั่งงาน Layout ของเมนูพวกนี้ให้เลื่อนออกไปจากนอกจอนั่นเอง เช่น เมนูอยู่ข้างบนก็จะเลื่อนขึ้นบนจนออกนอกจาก และถ้าอยู่ข้างล่างก็เลื่อนลงไปข้างล่างจนออกนอกจอเช่นกัน
สำหรับระยะในการเคลื่อนที่ก็คือขนาดความสูงของมันนั่นเอง เพราะเจ้าของบล็อกวางแถบพวกนี้ชิดขอบบน แต่สำหรับแถบ Header ที่อยู่ต่อจาก Action Bar ก็จะให้เลื่อนเป็นสองเท่าของความสูงแทน
จึงเป็นสาเหตว่าทำไมเจ้าของบล็อกกำหนดแถบพวกนี้ให้มีความสูงเท่ากันทั้งหมด เพื่อที่จะได้ง่ายต่อการคำนวณ ใช้ระยะทางเป็นสองเท่าของความสูงนั่นเอง แต่ถ้ากำหนดความสูงต่างกันก็ทำได้เหมือนกันนะ โดยใช้ความสูงของ Header บวกกับ Action Bar นั่นเอง
หัวใจสำคัญที่สุดคือ OnTouchListener
การจะรับรู้ได้ว่าผู้ใช้เลื่อนขึ้นหรือเลื่อนลงก็จะต้องใช้ OnTouchListener ไว้ที่ scrollView แล้วทำการคำนวณเอาว่าผู้ใช้เลื่อนขึ้นหรือเลื่อนลง และควรจะมีการเก็บสถานะไว้ด้วยว่าตอนนี้แถบเมนูซ่อนหรือแสดงอยู่ เพราะต้องคิดเผื่อในกรณีว่าผู้ใช้เลื่อนลงแล้วเมนูซ่อน จากนั้นผู้ใช้ก็เลื่อนลงไปอีกเรื่อยๆ
ถึงแม้ว่าการใช้ Object Animator จะช่วยให้ไม่ต้องกังวลเรื่อย Animation ไม่ต่อเนื่อง เพราะเมื่อเมนูซ่อนแล้ว ต่อให้ใช้คำสั่งซ่อนเมนูอีกครั้งก็จะไม่มีผล เพราะตำแหน่งของเมนูนั้นอยู่ในตำแหน่งที่ซ่อนเรียบร้อยแล้ว แต่ทว่าการจะให้คำสั่งมันสั่งงานซ้ำเรื่อยๆก็คงไม่เหมาะซักเท่าไร ทำให้เปลือง Process โดยใช่เหตุถึงแม้ว่าจะเล็กๆน้อยๆก็ตาม ดังนั้นจึงยอมเสียเวลาเกือบสถานะของแถบเมนูซักหน่อยก็ย่อมจะดีกว่า
หมายเหตุ - สำหรับตัวคำสั่งที่เน้นสีแดงจะอธิบายในตอนหลัง
จะเห็นว่าเจ้าของบล็อกประกาศตัวแปร DISTANCE ไว้ให้มีค่าเท่ากับ 3 ซึ่งนั้นหมายถึงว่าจะให้ผู้ใช้เลื่อนนิ้วไปเป็นระยะทางเท่าไรถึงจะซ่อนหรือแสดงเมนู โดย 3 ที่ว่านี้จะทำเป็นหน่วย dp ส่วนทำยังไงเดี๋ยวอธิบายให้ต่อไป
สำหรับตัวแปร startY เอาไว้เก็บตำแหน่งเริ่มต้น เมื่อผู้ใช้เริ่มแตะนิ้วลงบน Scroll View (ACTION_DOWN นั่นเอง) ส่วน dist ก็คือระยะทางที่ผู้ใช้ลาก Scroll View (ACTION_MOVE อีกนั่นเอง) ซึ่งค่า dist จะได้มาจากผลต่างระหว่างตำแหน่งนิ้วของผู้ใช้ ณ ปัจจุบันกับค่า startY ที่เก็บไว้ในตอนเริ่มต้นแตะที่ Scroll View และสุดท้ายคือ isMenuHide เอาไว้เช็คสถานะของเมนูว่าแสดงหรือซ่อนอยู่นั่นเอง
ส่วนการดู Action จะแบ่งออกเป็น 3 ประเภทคือ Down, Move และ Up
เมื่อเกิด ACTION_DOWN (ผู้ใช้เริ่มแตะที่ Scroll View) ก็จะเก็บค่าตำแหน่ง Y ณ ตรงนั้นไว้ในตัวแปร startY เพื่อนำไปใช้คำนวณเมื่อมีการลากนิ้ว
เมื่อเกิด ACTION_MOVE (ผู้ใช้เริ่มเลื่อนนิ้ว) จะทำการคำนวณหาค่า dist ทันทีว่าเป็นระยะทางเท่าไร ถ้าค่าที่ได้มีค่าติดลบเกิน -3dp นั่นหมายถึงผู้ใช้เลื่อนนิ้วขึ้น จากนั้นก็จะเช็คว่าเมนูซ่อนอยู่หรือไม่ ถ้าใช่ก็ค่อยแสดงเมนูขึ้นมา แล้วกำหนดสถานะว่าเมนูแสดงอยู่ แต่ถ้าค่า dist มีค่ามากกว่า 3dp นั่นหมายถึงผู้ใช้เลื่อนนิ้วลง ก็จะเช็คว่าเมนูแสดงอยู่หรือป่าว ถ้าแสดงอยู่ก็ทำการซ่อเมนูซะ แล้วกำหนดว่าเมนูถูกซ่อนอยู่
เท่านั้นยังไม่จบนะ เพราะว่าต้องอัปเดตตำแหน่งให้ startY ใหม่ด้วย โดยจะเช็คว่า ถ้าเมนูถูกซ่อนอยู่และค่า dist มีค่าน้อยกว่า -3dp ก็จะให้อัปเดตตำแหน่งใหม่ หรือ เมนูถูกแสดงอยู่และค่า dist มีค่ามากกว่า 0 ก็จะให้อัปเดตค่าตำแหน่งใหม่เช่นกัน
ที่ต้องเพิ่มโค๊ดแบบนี้ก็เพราะว่าเผื่อกรณีที่ผู้ใช้ลากนิ้วขึ้นจนเกิน 3dp แล้วเมนูซ่อนไปแล้ว แต่ว่าผู้ใช้ลากนิ้วขึ้นต่ออีก สมมติว่า 100dp ละกัน ถ้าผู้ใช้เลื่อนนิ้วลงต่อล่ะ? กลายเป็นว่าผู้ใช้ต้องลากนิ้วลงถึง -103dp แถบเมนูถึงจะแสดงซะงั้น ซึ่งในความเหมาะสมควรจะไม่สนว่าผู้ใช้ลากไกลเท่าไร แต่เมื่อเลื่อนลงจากเดิมแค่ -3dp เมนูก็ควรจะแสดง ดังนั้นคำสั่งที่เพิ่มเข้ามาจึงเหมือนกับอัปเดตค่าเมื่อผู้ใช้ลากนิ้วยาวๆนั่นเอง
เมื่อเกิด ACTION_UP (ผู้ใช้ยกนิ้วออก) ก็แค่เคลียร์ค่า startY ให้เป็นซะ แล้วรอผู้ใช้แตะ Scroll View ในครั้งต่อไป
ตอนจบ onTouch ทำไมต้อง Return ค่าเป็น False ล่ะ?
อันนี้ต้องรู้ก่อนว่าการ Return ตรงนี้หมายถึงอะไร ซึ่งการ Return ค่า True จะเป็นการบอกว่า Scroll View เกิด Listener ขึ้นแล้วนะ เพราะงั้น Listener ตัวอื่นไม่ต้องทำงานซ้ำแล้ว ผลก็คือเลื่อน Scroll View ไปมาไม่ได้ ดังนั้นการ Return เป็น False จึงเป็นเปรียบการทำให้ onTouch ทำงานซ้อนกับ Scroll ของ Scroll View ได้นั่นเอง
ฟังก์ชัน pxToDp มาจากไหน?
มันคือฟังก์ชันเอาไปแปลงหน่วย px ให้กลายเป็น dp นั่นเอง เป็นฟังก์ชันหากินที่เจ้าของบล็อกมีไว้ใช้งานอยู่บ่อยๆ ดังนั้นเมื่อเจ้าของบล็อกได้ค่าระยะทางที่ผู้ใช้เลื่อนนิ้วไปมา ก็จับแปลงเป็นหน่วย dp ซะ เท่านี้ก็รู้แล้วว่าลากนิ้วขึ้นหรือลงเกิน 3dp หรือป่าว
ต่อกันที่ Object Animator สำหรับแสดงและซ่อนแถบเมนู
สำหรับ Object Animator จริงๆแล้วไม่ค่อยยากหรอก เพราะเคยอธิบายไว้แล้วใน [Android Code] มาทำความรู้จักกับ Object Animator กันดีกว่า~! โดยเจ้าของบล็อกก็ประกาศ Layout ของแถบเมนูทั้งสามตัวก่อนดังนี้
แล้วจึงสร้างฟังก์ชัน hideMenuBar กับ showMenuBar ขึ้นมา เพื่อใช้สั่งงานเมื่อจะแสดงหรือซ่อนแถบเมนู โดยเอา Animator Set เข้ามาช่วยเพื่อให้สามารถสั่งงานได้พร้อมๆกัน
จะเห็นว่าระยะทางที่จะสั่งให้เคลื่อนที่ก็สามารถดึงค่าความสูงจาก Layout ได้เลย และเมื่ออยากให้กลับมาตำแหน่งเดิมก็กำหนดเป็น 0 ให้หมดก็เท่านั้นเอง โดยจะกำหนดให้ทำงานพร้อมๆกันโดยใช้เวลา 300 มิลลิวินาที
เท่านี้ก็เรียบร้อยแล้วววว~♪
สำหรับโค๊ดทั้งหมดก็จะมีดังนี้
MainActivity.java
ส่วน Layout คงไม่ต้องแปะหรอกเนอะ เพราะแปะไว้ที่ข้างบนแล้ว เพราะงั้นข้ามมาเป็น Android Manifest เลยดีกว่า
AndroidManifest.xml
ดาวน์โหลดไฟล์ตัวอย่าง
• Auto Hide Menu [GitHub]
• Auto Hide Menu [Google Drive]
• Auto Hide Menu [SleepingForLess]
บทความนี้ก็อยากให้ผู้ที่หลงเข้ามาอ่านได้รู้ว่าบางอย่างถึงจะดูเหมือนเสียเวลาทำ แต่ทว่ามันคือองค์ประกอบหนึ่งในความสมบูรณ์ ซึ่งคงจะดูไม่ดีแน่ๆถ้าแอปพลิเคชันมีนั้นไร้ชีวิตจิตใจ กดปุปเปลี่ยนหน้าปั๊ป ไม่มีความต่อเนื่องหรือยืดหยุ่น ดังนั้นถ้าเป็นไปได้ก็อยากให้เสียเวลากับเรื่องพวกนี้บ้าง เพื่อที่ว่าแอปพลิเคชันของผู้ที่หลงเข้ามาอ่านนั้นจะได้มีชีวิตมากขึ้น
สรุป ID ที่จะใช้ scrollView, layoutActionBar, layoutHeader และ layoutMenu
แล้ว Object Animator จะสั่งงานยังไงให้มันซ่อนเมนูได้ล่ะ?
คำถามนี้ทำเผื่อสำหรับผู้ที่หลงเข้ามาอ่านยังมองไม่ออกว่ามันจะทำได้ยังไง เจ้าของบล็อกก็จะให้ Object Animator ไปสั่งงาน Layout ของเมนูพวกนี้ให้เลื่อนออกไปจากนอกจอนั่นเอง เช่น เมนูอยู่ข้างบนก็จะเลื่อนขึ้นบนจนออกนอกจาก และถ้าอยู่ข้างล่างก็เลื่อนลงไปข้างล่างจนออกนอกจอเช่นกัน
สำหรับระยะในการเคลื่อนที่ก็คือขนาดความสูงของมันนั่นเอง เพราะเจ้าของบล็อกวางแถบพวกนี้ชิดขอบบน แต่สำหรับแถบ Header ที่อยู่ต่อจาก Action Bar ก็จะให้เลื่อนเป็นสองเท่าของความสูงแทน
จึงเป็นสาเหตว่าทำไมเจ้าของบล็อกกำหนดแถบพวกนี้ให้มีความสูงเท่ากันทั้งหมด เพื่อที่จะได้ง่ายต่อการคำนวณ ใช้ระยะทางเป็นสองเท่าของความสูงนั่นเอง แต่ถ้ากำหนดความสูงต่างกันก็ทำได้เหมือนกันนะ โดยใช้ความสูงของ Header บวกกับ Action Bar นั่นเอง
หัวใจสำคัญที่สุดคือ OnTouchListener
การจะรับรู้ได้ว่าผู้ใช้เลื่อนขึ้นหรือเลื่อนลงก็จะต้องใช้ OnTouchListener ไว้ที่ scrollView แล้วทำการคำนวณเอาว่าผู้ใช้เลื่อนขึ้นหรือเลื่อนลง และควรจะมีการเก็บสถานะไว้ด้วยว่าตอนนี้แถบเมนูซ่อนหรือแสดงอยู่ เพราะต้องคิดเผื่อในกรณีว่าผู้ใช้เลื่อนลงแล้วเมนูซ่อน จากนั้นผู้ใช้ก็เลื่อนลงไปอีกเรื่อยๆ
ถึงแม้ว่าการใช้ Object Animator จะช่วยให้ไม่ต้องกังวลเรื่อย Animation ไม่ต่อเนื่อง เพราะเมื่อเมนูซ่อนแล้ว ต่อให้ใช้คำสั่งซ่อนเมนูอีกครั้งก็จะไม่มีผล เพราะตำแหน่งของเมนูนั้นอยู่ในตำแหน่งที่ซ่อนเรียบร้อยแล้ว แต่ทว่าการจะให้คำสั่งมันสั่งงานซ้ำเรื่อยๆก็คงไม่เหมาะซักเท่าไร ทำให้เปลือง Process โดยใช่เหตุถึงแม้ว่าจะเล็กๆน้อยๆก็ตาม ดังนั้นจึงยอมเสียเวลาเกือบสถานะของแถบเมนูซักหน่อยก็ย่อมจะดีกว่า
ScrollView scrollView = (ScrollView)findViewById(R.id.scrollView);
scrollView.setOnTouchListener(new OnTouchListener() {
final int DISTANCE = 3;
float startY = 0;
float dist = 0;
boolean isMenuHide = false;
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
startY = event.getY();
} else if(action == MotionEvent.ACTION_MOVE) {
dist = event.getY() - startY;
if((pxToDp((int)dist) <= -DISTANCE) && !isMenuHide) {
isMenuHide = true;
hideMenuBar();
} else if((pxToDp((int)dist) > DISTANCE) && isMenuHide) {
isMenuHide = false;
showMenuBar();
}
if((isMenuHide && (pxToDp((int)dist) <= -DISTANCE))
|| (!isMenuHide && (pxToDp((int)dist) > 0))) {
startY = event.getY();
}
} else if(action == MotionEvent.ACTION_UP) {
startY = 0;
}
return false;
}
});
หมายเหตุ - สำหรับตัวคำสั่งที่เน้นสีแดงจะอธิบายในตอนหลัง
จะเห็นว่าเจ้าของบล็อกประกาศตัวแปร DISTANCE ไว้ให้มีค่าเท่ากับ 3 ซึ่งนั้นหมายถึงว่าจะให้ผู้ใช้เลื่อนนิ้วไปเป็นระยะทางเท่าไรถึงจะซ่อนหรือแสดงเมนู โดย 3 ที่ว่านี้จะทำเป็นหน่วย dp ส่วนทำยังไงเดี๋ยวอธิบายให้ต่อไป
สำหรับตัวแปร startY เอาไว้เก็บตำแหน่งเริ่มต้น เมื่อผู้ใช้เริ่มแตะนิ้วลงบน Scroll View (ACTION_DOWN นั่นเอง) ส่วน dist ก็คือระยะทางที่ผู้ใช้ลาก Scroll View (ACTION_MOVE อีกนั่นเอง) ซึ่งค่า dist จะได้มาจากผลต่างระหว่างตำแหน่งนิ้วของผู้ใช้ ณ ปัจจุบันกับค่า startY ที่เก็บไว้ในตอนเริ่มต้นแตะที่ Scroll View และสุดท้ายคือ isMenuHide เอาไว้เช็คสถานะของเมนูว่าแสดงหรือซ่อนอยู่นั่นเอง
ส่วนการดู Action จะแบ่งออกเป็น 3 ประเภทคือ Down, Move และ Up
เมื่อเกิด ACTION_DOWN (ผู้ใช้เริ่มแตะที่ Scroll View) ก็จะเก็บค่าตำแหน่ง Y ณ ตรงนั้นไว้ในตัวแปร startY เพื่อนำไปใช้คำนวณเมื่อมีการลากนิ้ว
เมื่อเกิด ACTION_MOVE (ผู้ใช้เริ่มเลื่อนนิ้ว) จะทำการคำนวณหาค่า dist ทันทีว่าเป็นระยะทางเท่าไร ถ้าค่าที่ได้มีค่าติดลบเกิน -3dp นั่นหมายถึงผู้ใช้เลื่อนนิ้วขึ้น จากนั้นก็จะเช็คว่าเมนูซ่อนอยู่หรือไม่ ถ้าใช่ก็ค่อยแสดงเมนูขึ้นมา แล้วกำหนดสถานะว่าเมนูแสดงอยู่ แต่ถ้าค่า dist มีค่ามากกว่า 3dp นั่นหมายถึงผู้ใช้เลื่อนนิ้วลง ก็จะเช็คว่าเมนูแสดงอยู่หรือป่าว ถ้าแสดงอยู่ก็ทำการซ่อเมนูซะ แล้วกำหนดว่าเมนูถูกซ่อนอยู่
เท่านั้นยังไม่จบนะ เพราะว่าต้องอัปเดตตำแหน่งให้ startY ใหม่ด้วย โดยจะเช็คว่า ถ้าเมนูถูกซ่อนอยู่และค่า dist มีค่าน้อยกว่า -3dp ก็จะให้อัปเดตตำแหน่งใหม่ หรือ เมนูถูกแสดงอยู่และค่า dist มีค่ามากกว่า 0 ก็จะให้อัปเดตค่าตำแหน่งใหม่เช่นกัน
ที่ต้องเพิ่มโค๊ดแบบนี้ก็เพราะว่าเผื่อกรณีที่ผู้ใช้ลากนิ้วขึ้นจนเกิน 3dp แล้วเมนูซ่อนไปแล้ว แต่ว่าผู้ใช้ลากนิ้วขึ้นต่ออีก สมมติว่า 100dp ละกัน ถ้าผู้ใช้เลื่อนนิ้วลงต่อล่ะ? กลายเป็นว่าผู้ใช้ต้องลากนิ้วลงถึง -103dp แถบเมนูถึงจะแสดงซะงั้น ซึ่งในความเหมาะสมควรจะไม่สนว่าผู้ใช้ลากไกลเท่าไร แต่เมื่อเลื่อนลงจากเดิมแค่ -3dp เมนูก็ควรจะแสดง ดังนั้นคำสั่งที่เพิ่มเข้ามาจึงเหมือนกับอัปเดตค่าเมื่อผู้ใช้ลากนิ้วยาวๆนั่นเอง
เมื่อเกิด ACTION_UP (ผู้ใช้ยกนิ้วออก) ก็แค่เคลียร์ค่า startY ให้เป็นซะ แล้วรอผู้ใช้แตะ Scroll View ในครั้งต่อไป
ตอนจบ onTouch ทำไมต้อง Return ค่าเป็น False ล่ะ?
อันนี้ต้องรู้ก่อนว่าการ Return ตรงนี้หมายถึงอะไร ซึ่งการ Return ค่า True จะเป็นการบอกว่า Scroll View เกิด Listener ขึ้นแล้วนะ เพราะงั้น Listener ตัวอื่นไม่ต้องทำงานซ้ำแล้ว ผลก็คือเลื่อน Scroll View ไปมาไม่ได้ ดังนั้นการ Return เป็น False จึงเป็นเปรียบการทำให้ onTouch ทำงานซ้อนกับ Scroll ของ Scroll View ได้นั่นเอง
ฟังก์ชัน pxToDp มาจากไหน?
มันคือฟังก์ชันเอาไปแปลงหน่วย px ให้กลายเป็น dp นั่นเอง เป็นฟังก์ชันหากินที่เจ้าของบล็อกมีไว้ใช้งานอยู่บ่อยๆ ดังนั้นเมื่อเจ้าของบล็อกได้ค่าระยะทางที่ผู้ใช้เลื่อนนิ้วไปมา ก็จับแปลงเป็นหน่วย dp ซะ เท่านี้ก็รู้แล้วว่าลากนิ้วขึ้นหรือลงเกิน 3dp หรือป่าว
public int pxToDp(int px) {
DisplayMetrics dm = this.getResources().getDisplayMetrics();
int dp = Math.round(px / (dm.densityDpi / DisplayMetrics.DENSITY_DEFAULT));
return dp;
}
ต่อกันที่ Object Animator สำหรับแสดงและซ่อนแถบเมนู
สำหรับ Object Animator จริงๆแล้วไม่ค่อยยากหรอก เพราะเคยอธิบายไว้แล้วใน [Android Code] มาทำความรู้จักกับ Object Animator กันดีกว่า~! โดยเจ้าของบล็อกก็ประกาศ Layout ของแถบเมนูทั้งสามตัวก่อนดังนี้
RelativeLayout layoutMenu, layoutActionBar, layoutHeader;
...
layoutMenu = (RelativeLayout)findViewById(R.id.layoutMenu);
layoutActionBar = (RelativeLayout)findViewById(R.id.layoutActionBar);
layoutHeader = (RelativeLayout)findViewById(R.id.layoutHeader);
แล้วจึงสร้างฟังก์ชัน hideMenuBar กับ showMenuBar ขึ้นมา เพื่อใช้สั่งงานเมื่อจะแสดงหรือซ่อนแถบเมนู โดยเอา Animator Set เข้ามาช่วยเพื่อให้สามารถสั่งงานได้พร้อมๆกัน
public void showMenuBar() {
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator anim1 = ObjectAnimator.ofFloat(layoutMenu
, View.TRANSLATION_Y, 0);
ObjectAnimator anim2 = ObjectAnimator.ofFloat(layoutActionBar
, View.TRANSLATION_Y, 0);
ObjectAnimator anim3 = ObjectAnimator.ofFloat(layoutHeader
, View.TRANSLATION_Y, 0);
animSet.playTogether(anim1, anim2, anim3);
animSet.setDuration(300);
animSet.start();
}
public void hideMenuBar() {
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator anim1 = ObjectAnimator.ofFloat(layoutMenu
, View.TRANSLATION_Y, layoutMenu.getHeight());
ObjectAnimator anim2 = ObjectAnimator.ofFloat(layoutActionBar
, View.TRANSLATION_Y, -layoutActionBar.getHeight());
ObjectAnimator anim3 = ObjectAnimator.ofFloat(layoutHeader
, View.TRANSLATION_Y, -layoutHeader.getHeight() * 2);
animSet.playTogether(anim1, anim2, anim3);
animSet.setDuration(300);
animSet.start();
}
จะเห็นว่าระยะทางที่จะสั่งให้เคลื่อนที่ก็สามารถดึงค่าความสูงจาก Layout ได้เลย และเมื่ออยากให้กลับมาตำแหน่งเดิมก็กำหนดเป็น 0 ให้หมดก็เท่านั้นเอง โดยจะกำหนดให้ทำงานพร้อมๆกันโดยใช้เวลา 300 มิลลิวินาที
เท่านี้ก็เรียบร้อยแล้วววว~♪
สำหรับโค๊ดทั้งหมดก็จะมีดังนี้
MainActivity.java
package app.akexorcist.autohidemenu;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
public class MainActivity extends Activity {
RelativeLayout layoutMenu, layoutActionBar, layoutHeader;
ScrollView scrollView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getActionBar().hide();
layoutMenu = (RelativeLayout)findViewById(R.id.layoutMenu);
layoutActionBar = (RelativeLayout)findViewById(R.id.layoutActionBar);
layoutHeader = (RelativeLayout)findViewById(R.id.layoutHeader);
scrollView = (ScrollView)findViewById(R.id.scrollView);
scrollView.setOnTouchListener(new OnTouchListener() {
final int DISTANCE = 3;
float startY = 0;
float dist = 0;
boolean isMenuHide = false;
public boolean onTouch(View v, MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
startY = event.getY();
} else if(action == MotionEvent.ACTION_MOVE) {
dist = event.getY() - startY;
if((pxToDp((int)dist) <= -DISTANCE) && !isMenuHide) {
isMenuHide = true;
hideMenuBar();
} else if((pxToDp((int)dist) > DISTANCE) && isMenuHide) {
isMenuHide = false;
showMenuBar();
}
if((isMenuHide && (pxToDp((int)dist) <= -DISTANCE))
|| (!isMenuHide && (pxToDp((int)dist) > 0))) {
startY = event.getY();
}
} else if(action == MotionEvent.ACTION_UP) {
startY = 0;
}
return false;
}
});
}
public int pxToDp(int px) {
DisplayMetrics dm = this.getResources().getDisplayMetrics();
int dp = Math.round(px / (dm.densityDpi
/ DisplayMetrics.DENSITY_DEFAULT));
return dp;
}
public void showMenuBar() {
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator anim1 = ObjectAnimator.ofFloat(layoutMenu
, View.TRANSLATION_Y, 0);
ObjectAnimator anim2 = ObjectAnimator.ofFloat(layoutActionBar
, View.TRANSLATION_Y, 0);
ObjectAnimator anim3 = ObjectAnimator.ofFloat(layoutHeader
, View.TRANSLATION_Y, 0);
animSet.playTogether(anim1, anim2, anim3);
animSet.setDuration(300);
animSet.start();
}
public void hideMenuBar() {
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator anim1 = ObjectAnimator.ofFloat(layoutMenu
, View.TRANSLATION_Y, layoutMenu.getHeight());
ObjectAnimator anim2 = ObjectAnimator.ofFloat(layoutActionBar
, View.TRANSLATION_Y, -layoutActionBar.getHeight());
ObjectAnimator anim3 = ObjectAnimator.ofFloat(layoutHeader
, View.TRANSLATION_Y, -layoutHeader.getHeight() * 2);
animSet.playTogether(anim1, anim2, anim3);
animSet.setDuration(300);
animSet.start();
}
}
ส่วน Layout คงไม่ต้องแปะหรอกเนอะ เพราะแปะไว้ที่ข้างบนแล้ว เพราะงั้นข้ามมาเป็น Android Manifest เลยดีกว่า
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="app.akexorcist.autohidemenu"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="19" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainActivity"
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 Hide Menu [GitHub]
• Auto Hide Menu [Google Drive]
• Auto Hide Menu [SleepingForLess]
บทความนี้ก็อยากให้ผู้ที่หลงเข้ามาอ่านได้รู้ว่าบางอย่างถึงจะดูเหมือนเสียเวลาทำ แต่ทว่ามันคือองค์ประกอบหนึ่งในความสมบูรณ์ ซึ่งคงจะดูไม่ดีแน่ๆถ้าแอปพลิเคชันมีนั้นไร้ชีวิตจิตใจ กดปุปเปลี่ยนหน้าปั๊ป ไม่มีความต่อเนื่องหรือยืดหยุ่น ดังนั้นถ้าเป็นไปได้ก็อยากให้เสียเวลากับเรื่องพวกนี้บ้าง เพื่อที่ว่าแอปพลิเคชันของผู้ที่หลงเข้ามาอ่านนั้นจะได้มีชีวิตมากขึ้น