สรุป session "Build your own custom view with Canvas API in Android" จากงาน Android Bangkok 2020
Session ของ Android GDE หนึ่งเดียวในไทยในตอนนี้ ที่เราเองน้านก็ดองนานแสนนานกว่าจะสรุปเนี่ย
Canvas API จะช่วยเราสร้าง custom view ได้อย่างไรกันนะ? session น้ีจะมีความต่อเนื่องกับงาน Android Bangkok รอบก่อนหน้านี้ ที่พูดเรื่อง Bitmap ไป แปะลิ้งบล็อกไป น่าจะ session สุดท้ายของงานเลยนะ
แล้ว Canvas คืออะไรหล่ะ? เราจะนึกถึงพื้นที่ในการวาดรูป จริงๆ Canvas เป็นเหมือน container ที่เก็บ operation ในการจะวาดภาพบนกระดาษ คือการเก็บคำสั่ง ที่จะวาดบนในกระดาษนั่นเอง
Component อะไร ที่เราวาดบนลง Android ได้บ้างนะ?
- Bitmap เป็นเหมือนกระดาษจริงๆ อะไรที่เราวาดก็จะอยู่ตรงนั้น
- Canvas บอกว่าจะใช้คำสั่งวาดอะไร เช่น วาดวงกลมลงในกระดาษให้หน่อยน้าา
- Drawing Primitive ลักษณะรูปร่างในการวาดรูปพื้นฐาน เช่น เป็นสี่เหลี่ยม เป็นเส้น เป็นตัวหนังสือ เป็น Bitmap
- Paint เปรียบเสมือนอุปกรณ์ที่เราใช้ในการวาด เช่น ใช้ดินสอ ใช้พู่กัน ใช้ปากกาตัดเส้น บลาๆ
ดังนั้น เส้นที่เราจะเขียนบน Canvas หรือ Bitmap นั้น จะขึ้นอยู่กับ Paint ที่เรากำหนดไว้นั่นเอง
Method พื้นฐานในการทำ CustomView ก็คือ onMeasure()
, onLayout()
และ onDraw()
โดย onDraw()
คือสิ่งที่เราจะวาด ลงบนใน CustomView ตัวนั้น โดยจะส่งตัว canvas
ออกมาให้ ดังนั้นเราจะวาดอะไรก็ได้ผ่าน canvas
ตัวนี้ได้เลย เพื่อสร้าง Bitmap ของเราเอง เพราะว่าสร้าง Bitmap หรือ Canvas ให้เราเรียบร้อยแล้ว
เบื้องหลังการ render view ให้เราดูนั้น ดูจาก session ที่ชื่อว่า "How Android renders" หนึ่งใน session ของ Google I/O 2018 นั่นเอง
Canvas
วิธีการสร้าง Canvas อย่างง่าย ทำได้โดยสร้าง Bitmap ขึ้นมา และใส่ลงใน Canvas เราจะได้ Canvas เพื่อเอาไปใช้ในการวาดรูปของเรา
Drawing Primitivess
การวาดจุด ก็จะใช้คำสั่งพื้นฐาน drawPoint()
กำหนดพิกัด x, y และ paint ในการวาดจุด
ถ้าอยากวาดเส้นหล่ะ? มีคำสั่ง drawLine()
ให้พิกัดเริ่มต้น พอกัดสิ้นสุด เพื่อลากเส้นจากจุดเริ่มต้นไปยังจุดปลายทางให้
ถ้าอยากวาดสี่เหลี่ยมหล่ะ? สร้างสี่เหลี่ยมขึ้นมาอันนึง ผ่าน drawRect()
กำหนด left, top, right, bottom ของมัน หรือจะใช้ class RectF()
ที่ใส่ค่าเป็น float
ก็ได้นะ ส่วน Rect()
ที่เราคุ้นเคยกันจะใส่ค่าเป็น Int
นะเออ
วิธีการวาดสี่เหลี่ยม เราเข้าใจว่าน่าจะประมาณนี้นะ คือ เอาจุดซ้ายบน และขวาล่างมา เพื่อตีกรอบเพื่อวาดเป็นสี่เหลี่ยม
วาดได้สี่เหลี่ยมขอบมนก็ทำได้เช่นกันนะ โดยการสร้างสี่เหลี่ยมปกติขึ้นมาก่อน จากนั้นใช้ drawRoundRect()
ในการทำขอบมน ซึ่ง redius สองตัวนี้เป็น redius แกน x และ redius แกน y นิยมใส่สองค่านี้เท่ากัน เพื่อความโค้งมนที่สวยงาม (หรือกันงงก็ไม่รู้เหมือนกันนะ)
แล้ววงกลมอ่ะ? กำหนดจุดศุนย์กลางของวงกลม ในพิกัดแกน x และ y พร้อมใส่รัศมีของวงกลมวงนั้น
และถ้า advance ขึ้นนิดนึงหล่ะ ถ้าเราจะวาด arc (ส่วนโค้งของวงกลม) หล่ะ? ตัวแปรจะเริ่มเยอะขึ้น เนื่องจาก เราไม่ได้กำหนดจุดศูนย์กลางของวงกลมเลย เราจะกำหนด left, top, right, bottom มันจะตีกรอบสี่เหลี่ยมให้เรา
แล้วรัศมีของวงกลมหล่ะ? มันจะเริ่มจากด้านขวามือก่อน เป็น 0 องศา แล้ววนตามเข็มนาฬิกาจนครบวง
วิธีการวาดเส้น จะให้วาดเริ่มที่ 150 องศา ให้เส้นยาวไป 180 องศา ทำให้เราได้เส้น arc ที่เราต้องการนั่นเอง
ของเราตอนนี้กลายเป็นวงกลมปิดเฉย ไม่ได้เป็นเส้น งงเหมือนกัน
useCenter
เป็น true มันจะเชื่อมจุด start กับจุด end
กำหนดสีพื้นหลัง สามารถกำหนดเป็น drawColor
ได้เลย ไม่ต้องวาดสี่เหลี่ยมเต็มหน้าจอเนอะ
อีกอันที่คุณเอกใช้บ่อยคือ path การวาดเส้น และเอาเส้น start กับเส้น end มาบรรจบกัน คล้ายๆกับ polygon ใน Google Maps เวลาจะวาด area
วิธีการวาด
moveTo(20f, 20f)
เลื่อน cursor ไปพิกัด (20, 20) จะอยู่มุมบนซ้ายมือlineTo(40f, 20f)
ลากเส้นจากมุมบนซ้ายมือไปที่ (40, 20)arcTo()
เขียนเส้น arc ขึ้นมาlineTo(90f, 20f)
ลากเส้นไปยังฝั่งมุมบนขวาlineTo(90f, 90f)
ลากเส้นจากมุมขวาบนลงมามุมขวาล่างlineTo(20f, 90f)
ลากเส้นด้านล่าง จากมุมล่างขวา ไปมุมล่างซ้ายclose()
มันจะวาดเส้นจากล่างซ้ายไปบนซ้ายที่เป็นจุดเริ่มต้น เป็นการจบการวาด
ต่อมาวาดข้อความ ให้ใส่ text พร้อมระบุพิกัดผ่าน drawText()
ถ้าจะเพิ่มรูป ให้โหลดไฟล์ภาพออกมาเป็น Bitmap และเอามาใส่ใน drawBitmap
พร้อมด้วยตำแหน่งที่เราต้องการวาดลงไป ใส่แค่ left กับ top ก็พอ เดี๋ยวมันจะวาดทั้งหมดให้เรา
ถ้าอยากให้วาดขนาดครึ่งนึงของรูปหล่ะ drawBitmap
จะคำนวณขนาดของ source และ destination ของ Bitmap ได้ เราสามารถ crop ภาพที่เป็น source ได้ด้วยนะ
Paint
เปรียบเสมือนอุปกรณ์ที่ใช้ในการวาด
สิ่งแรกที่เราจะกำหนดในการทำ Canvas ก็คือisAntiAlias = true
กำหนดเพื่อเส้นทแยงแตกเป็น pixel มันจะเกลี่ยให้เส้นมันสวยงามดูนวลขึ้น
อีกค่านึงที่ใช้บ่อย คือ style
มีทั้งหมด 3 แบบด้วยกัน
Paint.Style.FILL
พื้นที่ที่ถมสีที่เรากำหนดPaint.Style.STROKE
ถมสีเฉพาะเส้นขอบPaint.Style.FILL_AND_STROKE
ถมสีทั้งเส้นขอบและพื้นที่
กำหนดสีใช้ color
กำหนดความโปร่งใส่ใช้ alpha
กำหนดขนาดตัวหนังสือใช้ textSize
แต่มันเป็น pixel ดังนั้นถ้าใช้ sp
อย่าลืม convert เป็น pixel ด้วย และกำหนด font ด้วย typeface
ถามว่าไม่กำหนดได้ไหม ได้! แต่มันจะดึงจาก system มาให้ มันก็จะไม่ค่อยสวยหน่อย
ถ้าเราอยากรู้ว่ารูปตัวหนังสือที่เราจะวาด เกินหน้าจอไหม โดยยังไม่ต้อง render ให้ใช้ measureText
เพื่อวัดได้ว่าถ้าใช้ textSize
หรือ typeface
นี้ จะใช้พื้นที่ในการวาดเท่าไหร่?
Paint จะมีสิ่งนึงที่เรียกว่า Mask Filter อยู่ ตัวนึงที่คุณเอกใช้บ่อยๆคือ BlurMaskFilter
ทำวัตถุนั้นให้ blur
ถ้าจะลบทุกอย่างบน Canvas ทิ้งหล่ะ ใช้คำสั่ง drawColor
โดยการใส่สีขาว เพิ่มเติมด้วยการใส่ PorterDuff.Mode.CLEAR
Compositing Mode
เป็นเทคนิคผสมผสานภาพทั้งสองภาพเข้าด้วยกันในเชิง Digital Image ถูกคิดค้นโดย Thomas Porter และ Tom Duff อันเป็นที่มาของ PorterDuff นั่นเอง เพราะว่าทีม Android อาจจะให้เกียรติแก่คนคิดค้นการทำงานเหล่านี้ขึ้นมา
ตัว class นี้มีให้เราใช้งานกันอยู่แล้ว ไม่ต้อง import อะไรมาเพิ่ม
Compositing Mode ทำงานอย่างไร? เราจะมี 2 ภาพ ด้วยกัน คือ Source ภาพที่ได้จากการที่เราใช้คำสั่ง draw ต่างๆ และ Destination คือสิ่งที่อยู่บน Bitmap อยู่แล้ว
แล้ว Compositing Mode มีอะไรบ้าง? อ่าาาา ตามรูปในสไลด์นี้เลยจ้า มีทั้งหมด 18 แบบ เช่น
ADD
เราจะเห็น object ทั้งสองทับกันDST_IN
เวาดเฉพาะ source แต่แสดงพื้นที่ที่ destination มีอยู่แล้วCLEAR
ไม่มีอะไรเลย จึงใช้ในการลบภาพทุกอย่างใน Canvas นั่นเอง
จริงๆแล้วมันทำงานยังไงกันนะ? มาจากสมการเหล่านี้ คือ การคำนวณ alpha, การคำนวณ color เป็น RGB และตามมาด้วยสมการของ Compositing Mode นั้นๆ
ตัวอย่าง เรานำสีของทั้ง source และ destination เป็น ARGB แต่เราจะไม่คำนวณเป็น 255 นะ แปลงเป็น 1 เพราะ 255 เราจะใช้ในเชิง programming เท่านั้น
จากนั้นนำไปคำนวณในสมการ
เมื่อได้ผลลัพธ์แล้ว นำไปคูณ 255 กลับไป เพื่อเป็นเลขฐาน 16
ผลลัพธ์ที่ได้
Quiz Time
อันนี้คือ Compositing Mode ของอะไร?
ซึ่งคนที่อยู่ในงาน คนที่นั่งดูไลฟ์จากที่บ้าน ต่างก็ตอบผิดกันเป็นแถบ
เฉลยคือ SCREEN
นั่นเอง กว่าจะถูกคือคนในงานตอบจนจะหมดตัวเลือกที่ตอบได้แล้วอ่ะ
ถ้า 18 แบบนี้เราไม่ถูกใจ เราทำเองได้ไหม? ไม่ได้! เพราะบน Android ไม่ให้เรา custom ตัวสมการเอง
ประโยชน์อย่างนึงของ Compositing Mode ก็คือ ใน Paint มี transfer mode xfermode
เราสามารถสร้าง PorterDuff transfer mode ลงไปได้ ดังนั้นทุกคำสั่งที่เราวาด มันจะเป็น Compositing Mode โดยอัตโนมัตินั่นเอง
สิ่งที่ควรรู้เวลาจะวาดด้วย Canvas ลงบน Custom View
invalidate()
เป็นคำสั่งให้ View วาดใหม่ทันที เป็น UI Thread- ถ้าต้องการเรียกผ่าน Worker Thread ให้ใช้
postInvalidate()
- คำนวณเป็น px นะ
- คำสั่งทุกอย่างที่จะวาดใน
onDraw()
ให้ใช้เวลาตํ่ากว่า 16 ms - อย่าไปสร้าง object ใหม่ๆด้วย onDraw เยอะ เพราะเวลา
onDraw()
ถ้าเราไป new instance บ่อยๆ มันจะส่งผลต่อ perfomance ด้วย - ดังนั้นจึงกำหนดเป็น global variable ในตัวที่เราใช้บ่อยๆใน
onDraw()
ให้กำหนดรอไว้เลย ดีกว่าไปกำหนดใหม่หลายๆครั้ง - ถ้าใช้เป็น
ViewGroup
ให้ใช้setWillNotDraw(false)
ได้เลย เพราะว่าปกติViewGroup
จะไม่ draw ตัวเองในครั้งแรก เราใส่เพื่อบอกมันว่า ให้ draw ตัวเองด้วยนะ
ทำไมต้องทำคำสั่งใน onDraw() ภายใน 16 ms ด้วยหล่ะ?
เพราะมีโปรเจกชื่อว่า Butter มีสโลแกนว่า Smooth as Butter ลื่นเหมือนเนย
concept คืออยากเพิ่ม performance และ experience ของ user ให้รู้สึกว่า Android มันลื่น ก็เลยทำให้ตัว OS run อยู่ที่ 60fps
ถ้า 1 วินาทีรัน 60 เฟรม แล้ว 1 เฟรมจะต้องใช้เวลาเท่าไหร่? เท่ากับ 16 ms นั่นเอง
ดังนั้นเราจะต้องทำคำสั่งใดๆใน UI Thread ใช้เวลาตํ่ากว่า 16 ms เพื่อความลื่นนั่นเอง
มือถือรุ่นใหม่ๆ เช่น Pixel4 จะมี 90 fps ดังนั้นเราจะต้องให้ operation ต่างๆ ทำงานเร็วขึ้นกว่าเดิม
เราต้องทำถึง 120 fps ไหม? ไม่จำเป็น ทำถึง 60 fps เป็นมาตรฐานก็พอแล้ว
KTX Graphic
เราจะคุ้นเคยกับการใช้ ktx ต่างๆในโปรเจกของเรา ซึ่งในส่วนนี้ก็มีมาให้เราเหมือนกัน
และมีอะไรให้ใช้บ้างนะ?
เราสร้าง Bitmap ขึ้นมา เพื่อมาใส่ใน Cnavas ใน KTX เราสามารถใช้ applyCanvas
ได้เลย ซึ่งมันจะทำการสร้าง Canvas ให้อยู่เบื้องหลัง และใน scope มันจะโยน Canvas มาให้เราเอาไปใช้ต่อได้เลย
แปลงสีจาก HEX color ที่เป็น string สามารถใช้ toColorInt()
ได้เลย แต่ด้วยความที่มัน .toColorInt() มันค่อนข้างอันตราย แนะนำให้ใช้อย่างระมัดระวังนิดนึง
จากเดิมที่สร้าง PorterDuffXfermode()
ขึ้นมา สามารถเรียกเป็น PorterDuff.Mode.OVERLAY.toXfermode()
ได้เลย สั้นลงกว่าเดิมเยอะ
ตัวสี่เหลี่ยม ถ้าเราสร้างสี่เหลี่ยมที่ขนาดเท่ากัน ต่างกันเพียง Int
และ Float
สามารถใช้ toRectF()
และ toRect()
ในการ convert ได้ง่ายๆเลย
สามารถ get ค่า alpha, red, green, blue จากสีได้โดยตรงเลย
สามารถใช้ท่านี้ได้ สำหรับ API Level 26 ขึ้นไปเนอะ
Use Case : Coupon UI
ใช้ Canvas สร้าง Custom View คือ ตัว coupon ที่เป็น ViewGroup ที่สามารถใส่ content ข้างในได้
หน้าตาจะประมาณนี้ พอดีว่าเป็น background สีขาว แคปจอมายังไม่ค่อยเห็น แล้วจอที่งานน่าจะแยกออกยากนิดนุง มีเงา สามารถปรับความโค้งต่างๆได้ อยากให้มัน Flexible
เบื้องหลังจะแบ่งออกมาเป็น 3 layers คือ border, background และ shadow วาดทับกันจนเป็น coupon และมีขอบให้ด้วย
สิ่งที่ควรรู้ เราไม่สามารถวาดอะไรเกินขอบเขตที่เราวาดได้ ดังนั้นจะต้องมี padding เผื่อด้วยในการวาดเงา
จริงๆแล้ว มันเกิดจากการวาด canvas ง่ายๆ แต่ละ layer จะเป็น Paint เนอะ มี 3 ตัว และทั้ง 3 มี shape เหมือนกัน ดังนั้นสามารถใช้ Path ร่วมกันได้เลย เมื่อวาด Path แล้ว เราจะวาด Paint ทีละชั้น โดยเริ่มจากชั้นล่างสุดคือ shadowPaint
ตามมาด้วย backgroundPaint
และจบด้วย borderPaint
การสร้าง Path จะมีส่วนที่ยากคือขอบมน เพราะเราไม่สามารถสร้างสี่เหลี่ยมขอบมนได้เนื่องจากมีพื้นที่ครึ่งวงกลมที่ถูกตัดออกอยู่ ถามว่าใช้ได้ไหม? ได้! โดยใช้ Compositing Mode ในการสี่เกลี่ยมขอบมน แล้วใช้ Paint อีกตัวนึง เพื่อเอา Compositing Mode มาตัดส่วนเว้าครึ่งวงกลมออกได้
ในที่นี้จะใช้ Path วาดทีละเส้น
ส่วนของเงา ที่มองในสไลด์ไม่เห็น กำหนดเป็นสีดำที่มี alpha เป็น 40 (มีค่าระหว่าง 0 - 255) และใช้ BlurMaskFilter
เพื่อทำให้เบลอตรงขอบ เบลอออกไปข้างนอก
ส่วนของพื้นหลัง ไม่มีอะไรมาก แค่เทสีเข้าไป
ขอบก็เช่นกัน เทสีไปที่ขอบ
ในส่วน usecase เจ้า coupon ถามว่าเรากำหนด flexible ได้แค่ไหน ก็จะประมาณนี้แหละ สามารถ custom attribute ของ View ได้ตามใจชอบ และรองรับ StateListDrawable ด้วย
อ่านเพิ่มเติม
สำหรับโค้ดทางเราพยายามลองเล่นดู สามารถช่วยกัน contribute ได้เลย
Glassmorphism
เป็นอะไรที่ทางเราไม่เก็ท ว่ามันเป็น trend ฝั่ง design ได้อย่างไร?
คุณเอกบอกว่าเป็น design ที่จะมาในปี 2021 ถามว่าสรุป session ตอนจะหมดปีแล้ว ถามว่ามีไหม ไม่ค่อยเจอนะ ถ้าเจอน่าจะใน Apeboard อ่ะ ตอนนี้ก็ไม่แน่ใจว่าเขาเอาออกยัง แต่เป็น website นะไม่ใช้แอพ
หน้าตาก็จะเป็นแบบนี้
.
มันคือ concept ในการ design เป็นรูปกระจกฝ้า อะไรที่อยู่ด้านหลังกระจกก็จะเบลอ ไม่ใช่เบลอพื้นหลังทั้งหมดเน้อ
ปกติจะเบลอเต็ม แต่ Glassmorphism มันเบลอเฉพาะส่วน
ถามว่าบน Android ทำได้ไหม? คุณเอกได้ทำการถามน้องเบน คำตอบคือ ได้!! โดยการใช้ 3D Engine แต่ถ้าใช้แบบนี้ เราก็ไม่ใช่ Native Developer หล่ะ เป็น Game Engine ไปแล้ว
ก่อนอื่นเรามาเข้าใจ behavior ของ native กันก่อนเลย คุณ native รู้จักกระจกไหม เขาไม่รู้จัก แล้วถ้ารู้จัก จะรู้จักจากอะไรหล่ะ? มี limitation คือ Canvas คือเบื้องหลังในการเกิด View ใน Android ดังนั้นเราจะทำด้วย native ยากมาก เพราะตัวมันไม่สามารถบอกได้ว่า สิ่งที่วาดลงไป ช่วยทำตัวเองเป็นกระจกนะ มันจะรู้จักตัวเองแค่พื้นผิวธรรมดา เงาก็คือใช้การเบลอ สรุปคือตัว native ไม่ support นั่นเอง
ดังนั้นในการทำการเบลอเฉพาะส่วน จะเป็นการร่วมพลังของ Bitmap API, Canvas API และ RenderScript API นั่นเอง
Fully Blur ก็จะมีพื้นหลัง ซึ่งไม่จำเป็นต้องชัดมาก ใช้ขนาดเล็กๆไปเลยยังได้ เพราะยังไงต้องถูกเบลออยู่ดี บวกกับ UI ที่ป็น content
Partially blur ประกอบด้วย 3 ส่วนด้วยกัน คือภาพพื้นหลัง ที่ไม่เบลอ, ภาพพื้นหลัง ที่ทำให้เบลอเฉพาะส่วน และ UI ที่เป็น content
มาดูพื้นหลังกระจกฝ้ากัน จริงๆคือสีขาวโปร่งใสน้อยมากจนดูเป็นสีเทา
ตรงนี้จะมี 3 layers ด้วยกัน มี Shadow, Surface และ Border
ตัว Border จะเป็นสีขาว มีความโปร่งใสมากกว่า layer อื่นๆ ส่วน Surface เป็นสีขาวเช่นกัน แต่มีความโปร่งใสน้อยกว่า layer อื่นๆ ทำให้ความรู้สึกเหมือนกระจก และ Shadow มีเงานิดหน่อย
แล้วเราจะเบลอเฉพาะส่วนได้อย่างไร?
เริ่มจากดึงภาพ Bitmap ที่ View ด้านหลัง ทำแบบนี้เพื่อเผื่อการใส่ View ต่างๆใน ViewGroup นี้ แล้วค่อยดึง Bitmap ออกมา
ทำการเบลอภาพที่ได้ ด้วยการใช้ RenderScript ช่วยในการทำเบลอ เพราะคำนวณด้วย GPU ทำให้ performance จะดีกว่าใช้ Canvas ในกรณีนี้ ทำให้ระยะเวลาการทำเบลอเร็วมากกว่า ประมาณ 1-3 ms
setRadius
เราจะกำหนด radius ในการเบลอเท่าไหร่ก็ได้ ค่ายิ่งมาก ยิ่งเบลอมาก สูงสุดที่ 25 นะ ตํ่าสุดคือ 0 ก็คือไม่เบลอ ดังนั้นเราจะได้ Bitmap ที่มีความเบลอเป็นที่เรียบร้อย
แล้วเราต้อง capture content ด้วย โดยการ capture view เฉพาะที่เป็นกระจก มาใส่ใน Bitmap
จากนั้นลองใช้ Compositing Mode แต่ไม่มีสมการที่เราอยากได้ อยากได้ค่า alpha เป็น 1 เสมอ พอเอาไปทำแล้วได้ภาพที่จางมาก ดังนั้น dump pixel ออกมาจาก Bitmap และคำนวณหาพื้นที่ ที่เราจะถมเป็นสีขาว
เมื่อเราได้ Source และ Destination มาแล้ว เราจะกำหนด Paint ตัวใหม่ขึ้นมา และให้ xfermode
เป็น SRC_IN
สรุป ทำได้จริง แต่ไม่ support native และการ overlap ลองทำแล้ว performance แย่มาก ถ้ามันซ้อนกัน ซึ่งจริงๆไม่ทำกันเพราะ UX ไม่ดีเท่าไหร่ ทาง design อาจจะต้องการแค่นี้
ไม่สามารถจบได้ใน View เดียว และพื้นหลังต้องไม่ขยับ ไม่งั้นแตกแน่นอน 60fps อาจจะเอาไม่อยู่
ส่วนโค้ดสามารถเข้าไปส่องได้ที่นี่
สามารถ support ค่ากาแฟเจ้าของบล็อกได้ที่ปุ่มแดงส้มสุดน่ารักที่มุมซ้ายล่าง หรือกดปุ่มตรงนี้ก็ได้จ้า
กด follow Twitter เพื่อได้รับข่าวสารก่อนใคร เช่น สปอย content ใหม่ หรือสรุป content เร็วๆในนี้จ้า
ติดตามข่าวสารและบทความใหม่ๆได้ที่
download แอพอ่านบล็อกใหม่ของเราได้ที่นี่