63

因此,在尝试设置视图的背景可绘制对象时,我有些困惑。该代码依赖于知道视图的高度,所以我不能从onCreate()or调用它onResume(),因为getHeight()返回 0.onResume()似乎是我能得到的最接近的。我应该在哪里放置如下代码,以便在向用户显示时背景会发生变化?

    TextView tv = (TextView)findViewById(R.id.image_test);
    LayerDrawable ld = (LayerDrawable)tv.getBackground();
    int height = tv.getHeight(); //when to call this so as not to get 0?
    int topInset = height / 2;
    ld.setLayerInset(1, 0, topInset, 0, 0);
    tv.setBackgroundDrawable(ld);
4

9 回答 9

71

我不知道ViewTreeObserver.addOnPreDrawListener(),我在一个测试项目中尝试过。

使用您的代码,它看起来像这样:

public void onCreate() {
setContentView(R.layout.main);

final TextView tv = (TextView)findViewById(R.id.image_test);
final LayerDrawable ld = (LayerDrawable)tv.getBackground();
final ViewTreeObserver obs = tv.getViewTreeObserver();
obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw () {
        Log.d(TAG, "onPreDraw tv height is " + tv.getHeight()); // bad for performance, remove on production
        int height = tv.getHeight();
        int topInset = height / 2;
        ld.setLayerInset(1, 0, topInset, 0, 0);
        tv.setBackgroundDrawable(ld);

        return true;
   }
});
}

在我的测试项目onPreDraw()中被调用了两次,我认为在你的情况下它可能会导致无限循环。

您可以尝试setBackgroundDrawable()仅在TextView更改高度时调用:

private int mLastTvHeight = 0;

public void onCreate() {
setContentView(R.layout.main);

final TextView tv = (TextView)findViewById(R.id.image_test);
final LayerDrawable ld = (LayerDrawable)tv.getBackground();
final ViewTreeObserver obs = mTv.getViewTreeObserver();
obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
    @Override
    public boolean onPreDraw () {
        Log.d(TAG, "onPreDraw tv height is " + tv.getHeight()); // bad for performance, remove on production
        int height = tv.getHeight();
        if (height != mLastTvHeight) {
            mLastTvHeight = height;
            int topInset = height / 2;
            ld.setLayerInset(1, 0, topInset, 0, 0);
            tv.setBackgroundDrawable(ld);
        }

        return true;
   }
});
}

但这对于您要实现的目标来说听起来有点复杂,而且对性能也不是很好。

由 kcoppock 编辑

这就是我最终从这段代码中所做的事情。Gautier的回答让我明白了这一点,所以我宁愿接受这个修改后的答案,也不愿自己回答。我最终改用 ViewTreeObserver 的 addOnGlobalLayoutListener() 方法,就像这样(在 onCreate() 中):

final TextView tv = (TextView)findViewById(R.id.image_test);
ViewTreeObserver vto = tv.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    @Override
    public void onGlobalLayout() {
        LayerDrawable ld = (LayerDrawable)tv.getBackground();
        ld.setLayerInset(1, 0, tv.getHeight() / 2, 0, 0);
    }
});

似乎工作完美;我检查了 LogCat 并没有看到任何异常活动。希望就是这样!谢谢!

于 2010-12-09T13:13:14.193 回答
64

关于视图树观察者:

返回的 ViewTreeObserver 观察者不保证在此视图的生命周期内保持有效。如果此方法的调用者保留对 ViewTreeObserver 的长期引用,则它应始终检查 isAlive() 的返回值。

即使它是一个短暂的参考(仅用于测量视图并执行与您正在做的相同的事情),我已经看到它不再“活着”并引发异常。事实上,如果 ViewTreeObserver 上的每个函数不再“活着”,它都会抛出 IllegalStateException,所以你总是必须检查“isAlive”。我不得不这样做:

    if(viewToMeasure.getViewTreeObserver().isAlive()) {
        viewToMeasure.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                if(viewToMeasure.getViewTreeObserver().isAlive()) {
                    // only need to calculate once, so remove listener
                    viewToMeasure.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                }

                // you can get the view's height and width here

                // the listener might not have been 'alive', and so might not have been removed....so you are only
                // 99% sure the code you put here will only run once.  So, you might also want to put some kind
                // of "only run the first time called" guard in here too                                        

            }
        });            
    }

这对我来说似乎很脆弱。很多事情——尽管很少——似乎都会出错。

所以我开始这样做:当你向 View 发布消息时,只有在 View 完全初始化(包括被测量)之后才会传递消息。如果您只希望您的测量代码运行一次,并且您实际上不想在视图的生命周期内观察布局更改(因为它没有被调整大小),那么我只是将我的测量代码放在这样的帖子中:

 viewToMeasure.post(new Runnable() {

        @Override
        public void run() {
            // safe to get height and width here               
        }

    });

我对视图发布策略没有任何问题,我还没有看到任何屏幕闪烁或其他您预计可能会出错的事情(但...)

我在生产代码中发生了崩溃,因为我的全局布局观察器没有被删除,或者当我尝试访问它时布局观察器没有“活着”

于 2013-03-08T18:45:56.983 回答
12

通过使用全局侦听器,您可能会遇到无限循环。

我通过调用解决了这个问题

whateverlayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);

在侦听器的 onGlobalLayout 方法内部。

于 2012-12-18T21:09:54.837 回答
4

在我的项目中,我使用以下代码段:

public static void postOnPreDraw(Runnable runnable, View view) {
    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
        @Override
        public boolean onPreDraw() {
            try {
                runnable.run();
                return true;
            } finally {
                view.getViewTreeObserver().removeOnPreDrawListener(this);
            }
        }
    });
}

所以,在里面提供了Runnable#run你可以确定View被测量和执行的相关代码。

于 2014-06-03T14:11:34.880 回答
3

您可以为 TextView 覆盖 onMeasure:

public MyTextView extends TextView {
   ...
   public void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec,heightMeasureSpec); // Important !!!
      final int width = getMeasuredHeight();
      final int height = getMeasuredHeight();
      // do you background stuff
   }
   ...
}

我几乎可以肯定你也可以在没有任何代码行的情况下做到这一点。我认为有机会创建图层列表。

于 2010-12-09T00:47:09.563 回答
1
textView.doOnPreDraw {
   //call textView.height
}

您还可以使用此视图扩展:

androidx.core.view (ViewKt.class)

public inline fun View.doOnPreDraw(
    crossinline action: (View) → Unit
): OneShotPreDrawListener

在即将绘制视图树时执行给定的操作。该动作只会在下一次抽奖之前被调用一次,然后被删除。

于 2022-02-04T12:30:55.093 回答
1

总而言之,处理视图大小的 3 种方法......嗯,也许是这样!

1)正如@Gautier Hayoun 提到的,使用 OnGlobalLayoutListener 监听器。但是,请记住在侦听器的第一个代码处调用:listViewProduct.getViewTreeObserver().removeOnGlobalLayoutListener(this);以确保此侦听器不会无限调用。还有一件事,有时这个监听器根本不会调用,因为你的视图被绘制在你不知道的地方。onCreate()因此,为了安全起见,让我们在方法(或onViewCreated()片段)中声明视图后立即注册此侦听器

view = ([someView]) findViewById(R.id.[viewId]);

// Get width of view before it's drawn.
view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
      @Override
      public void onGlobalLayout() {

        // Make this listener call once.
        view.getViewTreeObserver().removeOnGlobalLayoutListener(this);

        int width = listViewProduct.getWidth();
      });

2)如果您想在视图绘制并显示后立即做某事,只需使用post()方法(当然您可以在此处获取大小,因为您的视图已经完全绘制)。例如,

listViewProduct.post(new Runnable() {
  @Override
  public void run() {
    // Do your stuff here.
  }
});

此外,您可以使用postDelay()相同的目的,增加一点延迟时间。让我们自己尝试一下。你有时会需要它。例如,当您想在添加更多项目后自动滚动滚动视图scroll view或扩展或使更多视图在scroll view动画中的消失状态中可见时(这意味着此过程将花费一些时间来显示所有内容)。它肯定会改变scroll view' 的大小,所以在scroll view绘制之后,我们还需要等待一段时间才能获得确切的大小,因为它会增加更多的视图。现在是postDelay()方法来救援的时候了!

3)如果你想建立自己的视图,你可以创建一个类,并extend从任何你喜欢的视图中创建一个类,它们都有onMeasure()定义视图大小的方法。

于 2017-06-20T04:54:27.780 回答
0

您可以在 onCreate() 方法下面的类主要活动方法中获取所有视图度量:

@Override
protected void onCreate(Bundle savedInstanceState){}
@Override
public void onWindowFocusChanged(boolean hasFocus){
    int w = yourTextView.getWidth();
}

所以你可以重绘、排序……你的观点。:)

于 2015-01-31T08:40:36.770 回答
0

使用OnPreDrawListener() 代替addOnGlobalLayoutListener(),因为它被更早地调用。

  tv.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener()
    {
        @Override
        public boolean onPreDraw()
        {
            tv.getViewTreeObserver().removeOnPreDrawListener(this);
            // put your measurement code here
            return false;
        }
    });
于 2017-03-22T09:47:58.107 回答