คุณจะบอกได้อย่างไรเมื่อมีการวาดโครงร่าง?


201

ฉันมีมุมมองที่กำหนดเองที่วาดบิตแมปที่เลื่อนได้ไปที่หน้าจอ เพื่อเริ่มต้นฉันต้องผ่านขนาดเป็นพิกเซลของวัตถุเค้าโครงหลัก แต่ในระหว่างฟังก์ชั่น onCreate และ onResume ยังไม่มีการวาดเค้าโครงและดังนั้น layout.getMeasuredHeight () จึงส่งกลับ 0

ฉันได้เพิ่มตัวจัดการเพื่อรอหนึ่งวินาทีแล้วจึงวัด มันใช้งานได้ แต่มันเลอะเทอะและฉันก็ไม่รู้เลยว่าฉันจะตัดเวลาได้เท่าไหร่ก่อนที่ฉันจะจบลงก่อนที่โครงร่างจะถูกวาด

สิ่งที่ฉันต้องการรู้คือฉันจะตรวจจับได้อย่างไรเมื่อมีการวาดเลย์เอาต์? มีเหตุการณ์หรือการติดต่อกลับหรือไม่

คำตอบ:


391

คุณสามารถเพิ่มผู้สังเกตการณ์ต้นไม้ให้กับเลย์เอาต์ นี่ควรกลับความกว้างและความสูงที่ถูกต้อง onCreate()ถูกเรียกใช้ก่อนโครงร่างของมุมมองชายด์เสร็จสิ้น ดังนั้นความกว้างและความสูงจึงยังไม่ถูกคำนวณ ในการรับความสูงและความกว้างให้วางonCreate()วิธีนี้:

    final LinearLayout layout = (LinearLayout) findViewById(R.id.YOUR_VIEW_ID);
    ViewTreeObserver vto = layout.getViewTreeObserver(); 
    vto.addOnGlobalLayoutListener (new OnGlobalLayoutListener() { 
        @Override 
        public void onGlobalLayout() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    layout.getViewTreeObserver()
                            .removeOnGlobalLayoutListener(this);
                } else {
                    layout.getViewTreeObserver()
                            .removeGlobalOnLayoutListener(this);
                }
            int width  = layout.getMeasuredWidth();
            int height = layout.getMeasuredHeight(); 

        } 
    });

28
ขอบคุณมาก! แต่ฉันสงสัยว่าทำไมไม่มีวิธีที่ง่ายกว่าสำหรับการทำเช่นนี้เพราะมันเป็นปัญหาที่พบบ่อย
felixd

1
น่าอัศจรรย์ ฉันลอง OnLayoutChangeListener และใช้งานไม่ได้ แต่ OnGlobalLayoutListener ทำงาน ขอบคุณมาก!
YoYoMyo

8
removeGlobalOnLayoutListener เลิกใช้แล้วในระดับ API 16. ใช้ removeOnGlobalLayoutListener แทน
tounaobun

3
หนึ่ง "gotcha" ที่ฉันเห็นคือถ้ามุมมองของคุณไม่ปรากฏ onGlobalLayout จะถูกเรียกใช้ แต่ต่อมาเมื่อมองเห็น getView จะถูกเรียก แต่ไม่ใช่ onGlobalLayout ...
Stephen McCormick

1
อีกวิธีแก้ปัญหาอย่างรวดเร็วสำหรับสิ่งนี้คือการใช้view.post(Runnable)มันทำความสะอาดและทำสิ่งเดียวกัน
dvdciri

74

วิธีที่ง่ายจริงๆคือเพียงแค่โทรหาpost()เลย์เอาต์ของคุณ การดำเนินการนี้จะเรียกใช้รหัสของคุณในขั้นตอนถัดไปหลังจากสร้างมุมมองของคุณแล้ว

YOUR_LAYOUT.post( new Runnable() {
    @Override
    public void run() {
        int width  = YOUR_LAYOUT.getMeasuredWidth();
        int height = YOUR_LAYOUT.getMeasuredHeight(); 
    }
});

4
ฉันไม่รู้ว่าโพสต์จะเรียกใช้รหัสของคุณหรือไม่หลังจากสร้างมุมมองแล้ว ไม่มีการรับประกันเนื่องจากโพสต์จะเพิ่ม Runnable ที่คุณสร้างใน RunQueue เพื่อดำเนินการหลังจากคิวก่อนหน้านี้ถูกเรียกใช้งานบน UI Thread
HendraWD

4
ในทางตรงกันข้ามโพสต์ () จะดำเนินการหลังจากอัปเดตขั้นตอน UI ถัดไปซึ่งเกิดขึ้นหลังจากสร้างมุมมอง ดังนั้นคุณรับประกันได้ว่ามันจะทำงานหลังจากที่ดู
Elliptica

ฉันทดสอบโค้ดนี้หลายครั้งมันทำงานได้ดีที่สุดใน UI-thread เท่านั้น ในเธรดพื้นหลังบางครั้งก็ไม่ทำงาน
CoolMind

3
@HendraWijayaDjiono อาจจะมีคำตอบนี้: stackoverflow.com/questions/21937224/…
CoolMind

1
ฉันพบปัญหาเมื่อใช้คำตอบแรกไม่ทำงานทุกครั้งโดยใช้การโพสต์ในมุมมองที่ฉันต้องการเลื่อน (มุมมองเลื่อน) อนุญาตให้ฉันเลื่อนไปยังตำแหน่งที่ต้องการได้อย่างถูกต้องทันทีที่มุมมองพร้อมใช้งาน การเรียกใช้เมธอด scrollTo
Danuofr

21

ในการหลีกเลี่ยงรหัสและคำเตือนที่เลิกใช้คุณสามารถใช้:

view.getViewTreeObserver().addOnGlobalLayoutListener(
        new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressWarnings("deprecation")
            @Override
            public void onGlobalLayout() {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                    view.getViewTreeObserver()
                            .removeOnGlobalLayoutListener(this);
                } else {
                    view.getViewTreeObserver()
                            .removeGlobalOnLayoutListener(this);
                }
                yourFunctionHere();
            }
        });

1
ในการใช้รหัสนี้ต้องมีการประกาศ 'มุมมอง' เป็น 'ขั้นสุดท้าย'
BeccaP

2
ใช้เงื่อนไขนี้แทน Build.VERSION.SDK_INT <Build.VERSION_CODES.JELLY_BEAN
HendraWD

4

ดีขึ้นด้วยฟังก์ชั่นการขยาย kotlin

inline fun View.waitForLayout(crossinline yourAction: () -> Unit) {
    val vto = viewTreeObserver
    vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
        override fun onGlobalLayout() {
            when {
                vto.isAlive -> {
                    vto.removeOnGlobalLayoutListener(this)
                    yourAction()
                }
                else -> viewTreeObserver.removeOnGlobalLayoutListener(this)
            }
        }
    })
}

วิธีการแก้ปัญหาที่ดีและสง่างามที่สามารถนำมาใช้ใหม่
Ionut Negru


3

อีกทางเลือกหนึ่งสำหรับวิธีการทั่วไปคือเชื่อมต่อกับภาพวาดของมุมมอง

OnPreDrawListenerถูกเรียกหลายครั้งเมื่อแสดงมุมมองดังนั้นจึงไม่มีการวนซ้ำที่เฉพาะเจาะจงซึ่งมุมมองของคุณมีความกว้างหรือความสูงที่วัดได้ที่ถูกต้อง สิ่งนี้ต้องการให้คุณตรวจสอบอย่างต่อเนื่อง ( view.getMeasuredWidth() <= 0) หรือกำหนดขีด จำกัด ตามจำนวนครั้งที่คุณตรวจสอบว่ามีmeasuredWidthค่ามากกว่าศูนย์

นอกจากนี้ยังมีโอกาสที่จะไม่มีการดึงมุมมองซึ่งอาจบ่งบอกถึงปัญหาอื่น ๆ เกี่ยวกับรหัสของคุณ

final View view = [ACQUIRE REFERENCE]; // Must be declared final for inner class
ViewTreeObserver viewTreeObserver = view.getViewTreeObserver();
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw() {
        if (view.getMeasuredWidth() > 0) {     
            view.getViewTreeObserver().removeOnPreDrawListener(this);
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            //Do something with width and height here!
        }
        return true; // Continue with the draw pass, as not to stop it
    }
});

คำถามคือHow can you tell when a layout has been drawn?และคุณกำลังให้วิธีการที่คุณเรียกOnPreDrawListenerและdon't stop interrogating that view until it has provided you with a reasonable answerเพื่อคัดค้านการแก้ปัญหาของคุณควรชัดเจน
รถเข็นที่ถูกทิ้งร้าง

สิ่งที่คุณต้องรู้ไม่ใช่เมื่อมันถูกวาดขึ้น แต่เมื่อมันถูกวัด รหัสของฉันตอบคำถามที่ต้องถามจริงๆ
jwehrle

มีปัญหากับวิธีนี้ไหม? ไม่สามารถแก้ไขปัญหาที่พบได้หรือไม่
jwehrle

1
มุมมองจะไม่ถูกวัดจนกว่าจะมีการจัดวางดังนั้นทางเลือกนี้จึงมากเกินไปและทำการตรวจสอบซ้ำซ้อน คุณยอมรับว่าไม่ได้ตอบคำถาม แต่ควรถามสิ่งที่คุณรู้สึก ความคิดเห็นดั้งเดิมทำให้ทั้งสองประเด็นนี้ แต่ก็มีปัญหาบางประการเกี่ยวกับโค้ดที่ถูกส่งเป็นการแก้ไข
รถเข็นที่ถูกทิ้งร้าง

นั่นเป็นการแก้ไขที่ดี ขอบคุณ. ฉันเดาสิ่งที่ฉันถามคือคุณคิดว่าคำตอบนี้ควรถูกลบหรือไม่? ฉันพบปัญหาของ OP วิธีนี้แก้ปัญหาได้ ฉันเสนอโดยสุจริต นั่นเป็นความผิดพลาดหรือเปล่า? เป็นสิ่งที่ไม่ควรทำใน StackOverflow หรือไม่
jwehrle

2

เพียงตรวจสอบโดยเรียกวิธีการโพสต์ในรูปแบบหรือมุมมองของคุณ

 view.post( new Runnable() {
     @Override
     public void run() {
        // your layout is now drawn completely , use it here.
      }
});

สิ่งนี้ไม่เป็นความจริงและอาจซ่อนปัญหาที่เป็นไปได้ postจะรอคิวที่ runnable เพื่อรันหลังจากนั้นมันไม่แน่ใจว่ามุมมองนั้นถูกวัด
Ionut Negru

@IonutNegru อาจอ่านstackoverflow.com/a/21938380
Bruno Bieri

@BrunoBieri คุณสามารถทดสอบลักษณะการทำงานนี้ด้วยฟังก์ชั่น async ที่ปรับเปลี่ยนการวัดมุมมองและดูว่าคุณกำลังจัดคิวงานที่จะดำเนินการหลังจากแต่ละการวัดและรับค่าที่คาดหวัง
Ionut Negru

1

เมื่อonMeasureเรียกว่ามุมมองจะได้รับความกว้าง / ความสูงที่วัดได้ layout.getMeasuredHeight()หลังจากนี้คุณสามารถโทรหา


4
คุณไม่ต้องทำมุมมองที่กำหนดเองของคุณเองเพื่อที่จะรู้เมื่อ onMeasure fires?
Geeks On Hugs

ใช่คุณต้องใช้มุมมองที่กำหนดเองเพื่อเข้าถึงแบบวัด
เทรเวอร์ฮาร์ท
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.