屏幕方向改变时ImageView的Drawable忽略了setBounds并返回原始状态

5

编辑 3:

出于历史原因,我将保留下面的原始问题。但是,我发现这个问题不仅限于FrameLayout。因此,我更新了标题。

为了避免在此帖子中添加更多代码,我创建了一个演示该问题的示例项目,并将其上传到Google项目托管上。

问题的摘要如下:

在纵向方向上具有一定边界的可绘制对象,在改变方向并返回纵向时,不会保留设置的边界。相反,它会恢复到原始边界。尽管强制在方向更改时设置了明确的边界,但请注意,稍后在单击等操作中设置的任何边界都将被遵守。

我还上传了一个包含两个独立活动的应用程序版本来说明这个问题。

  1. 普通的活动,只是说明问题。
  2. ImageView上使用自定义BitmapDrawable的活动-这个版本在每次更改边界时都会打印到日志中。

第二个版本清楚地显示,即使我在可绘制对象上设置了边界,这些边界也不会在onBoundsChange()中遵守。


原始问题:

我正在使用一个FrameLayout,其中有2个重叠的ImageView用于显示“电池状态”图形。这是在纵向模式下。在横向模式下,我显示不同的布局(图表)。

我的问题是这样的-假设电池状态正在显示-例如30%。现在,我旋转屏幕并显示图表。当我回到纵向方向时,电池图形返回到其原始位置(即“满”)。

我尝试了各种方法来尝试弄清楚发生了什么。调试显示,“顶部”图形的边界确实按预期设置。因此,这似乎是一个失效问题。我提供了两个类和两个布局XML的代码(所有内容都经过简化),以便能够重新生成该问题。还附加了用于ImageView的占位符PNG。

Placeholder Battery Frame graphic Placeholder Battery Content graphic

有人能看出错误吗?要重现此问题,请运行应用程序,然后单击“更新”按钮。图形将会被“填充”到某个级别。然后,切换到横向模式,再切回纵向模式。图形不会记住先前的值。

该活动:

public class RotateActivity extends Activity {

    private View portraitView, landscapeView;
    private LayoutInflater li;
    private Configuration mConfig;
    private ValueIndicator indicator;
    private Button btn;
    private Random random = new java.util.Random();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        li = LayoutInflater.from(this);
        portraitView = li.inflate(R.layout.portrait, null);
        landscapeView = li.inflate(R.layout.landscape, null);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        mConfig = newConfig;
        initialize();

    }

    @Override
    protected void onResume() {
        mConfig = getResources().getConfiguration();
        initialize();
        super.onResume();
    }

    private void initialize(){
        if(mConfig.orientation == Configuration.ORIENTATION_LANDSCAPE){
            displayLandscape();
        } else {
            displayPortrait();
        }
    }

    private void displayLandscape() {
        setContentView(landscapeView);
    }

    private void displayPortrait() {
        setContentView(portraitView);
        btn = (Button)portraitView.findViewById(R.id.button1);
        indicator = (ValueIndicator)portraitView.findViewById(R.id.valueIndicator1);
    /*
     * Forcing the graphic to perform redraw to its known state when we return to portrait view.
     */
    indicator.updateIndicatorUi();  



        btn.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                updateIndicator(random.nextInt(100));
            }
        });

    }

    private void updateIndicator(int newValue){
        indicator.setPercent(newValue);
    }
}

portrait.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <com.gs.kiran.trial.inval.ValueIndicator
                android:id="@+id/valueIndicator1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="10dp" />

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Update" />

</LinearLayout>

ValueIndicator.java

public class ValueIndicator extends FrameLayout {

    private ImageView ivFrame, ivContent;
    private Drawable drFrame, drContent;
    private Rect mBounds;
    private int currentTop;
    private int mPercent;

    public ValueIndicator(Context context, AttributeSet attrs) {
        super(context, attrs);
        LayoutInflater li = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View root = li.inflate(R.layout.indicator, this, true);

        ivFrame = (ImageView) root.findViewById(R.id.ivFrame);
        ivContent = (ImageView) root.findViewById(R.id.ivContent);

        drFrame = ivFrame.getDrawable();
        drContent = ivContent.getDrawable();

        mBounds = drFrame.getBounds();

        Log.d(Constants.LOG_TAG, "Constructor of ValueIndicator");

    }

    public void setPercent(int newPercent){
       this.mPercent = newPercent;
       updateIndicatorUi();
    }

    public void updateIndicatorUi(){
        Rect newBounds = new Rect(mBounds);
        newBounds.top = mBounds.bottom - (int)(this.mPercent * mBounds.height() / 100);
        currentTop = newBounds.top;
        Log.d(Constants.LOG_TAG, "currentTop = "+currentTop);
        drContent.setBounds(newBounds);
        //invalidateDrawable(drContent);
        invalidate();
    }
}

indicator.xml(自定义视图中使用的FrameLayout的XML)

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/frameLayout1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >

    <ImageView
        android:id="@+id/ivFrame"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/frame" />

    <ImageView
        android:id="@+id/ivContent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/content" />

</FrameLayout>

landscape.xml(虚拟占位符 - 能够重现问题)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Landscape" />

</LinearLayout>

AndroidManifest.xml片段:

<activity
            android:label="@string/app_name"
            android:name=".RotateActivity" 
            android:configChanges="orientation|keyboardHidden">
            <intent-filter >
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

编辑

我还尝试调用setPercent()并在displayPortraitView()中传递保存的值-基本上是在回到纵向模式时强制更新到已知状态。 仍然没有运气。 请注意,日志告诉我绘图对象的边界是正确的。 我不明白为什么无效化不会发生。


第二次编辑:

  1. 在ValueIndicator.java中,我引入了一个成员变量mPercent,它始终存储上一个已知的百分比值。
  2. 更新setPercent()代码以更新成员变量mPercent; 然后调用updateIndicateUi()方法。
  3. updateIndicatorUi()(现在是public方法)现在使用该状态(即mPercent)来完成其工作。
  4. 每当我们回到竖屏模式时,我都会调用updateIndicatorUi()。 这会强制电池图形更新自己。

我还更新了代码以反映这些更改。

想法是每当我们从横向模式返回纵向模式时都强制重绘和无效化。 再次强调-我确实看到电池“内容”绘图对象的边界被设置为期望值,但是UI无法跟上。

3个回答

2
我已经查看了您在Google Code Hosting上的代码(非常感谢您对代码进行如此详尽的文档记录),发现当您从横屏切换回竖屏时,Drawable的边界确实再次改变了。
Drawable的边界不是由您的代码更改的,而是由ImageView的布局方法更改的。当您放置一个新布局(setContentView)时,所有布局代码都会运行,包括ImageView的。ImageView会更改其包含的可绘制对象的边界,这就是为什么您会得到可绘制对象的边界被改回原始状态的原因。
导致边界更改的堆栈跟踪如下:
Thread [<1> main] (Suspended (entry into method onBoundsChange in BitmapDrawable))  
    BitmapDrawable.onBoundsChange(Rect) line: 293   
    BitmapDrawable(Drawable).setBounds(int, int, int, int) line: 131    
    ImageView.configureBounds() line: 769   
    ImageView.setFrame(int, int, int, int) line: 742    
    ImageView(View).layout(int, int, int, int) line: 7186   
    LinearLayout.setChildFrame(View, int, int, int, int) line: 1254 
    LinearLayout.layoutVertical() line: 1130    
    LinearLayout.onLayout(boolean, int, int, int, int) line: 1047   
    LinearLayout(View).layout(int, int, int, int) line: 7192    
    FrameLayout.onLayout(boolean, int, int, int, int) line: 338 
    FrameLayout(View).layout(int, int, int, int) line: 7192 
    LinearLayout.setChildFrame(View, int, int, int, int) line: 1254 
    LinearLayout.layoutVertical() line: 1130    
    LinearLayout.onLayout(boolean, int, int, int, int) line: 1047   
    LinearLayout(View).layout(int, int, int, int) line: 7192    
    PhoneWindow$DecorView(FrameLayout).onLayout(boolean, int, int, int, int) line: 338  
    PhoneWindow$DecorView(View).layout(int, int, int, int) line: 7192   
    ViewRoot.performTraversals() line: 1145 
    ViewRoot.handleMessage(Message) line: 1865  
    ViewRoot(Handler).dispatchMessage(Message) line: 99 
    Looper.loop() line: 130 
    ActivityThread.main(String[]) line: 3835    
    Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]  
    Method.invoke(Object, Object...) line: 507  
    ZygoteInit$MethodAndArgsCaller.run() line: 847  
    ZygoteInit.main(String[]) line: 605 
    NativeStart.main(String[]) line: not available [native method]  

在阅读您的代码时,我发现为了绘制一个仪表盘而改变边界并存储边界等操作有些繁琐。我可以提出以下建议之一:
  1. 改变ImageView本身的大小(使用setLayoutParams),而不是其可绘制对象的边界。
  2. 不要使用ImageView,而是创建一个继承View的类,并覆盖onDraw(Canvas)方法,然后使用drawRect绘制红色矩形。

感谢您的输入,非常感激。实际上,这是一张“虚拟”的图片。我的真正应用程序使用FrameLayout显示电池状态,其中包含3个高质量的图像 - 电池“框架”,黑色的电池“背景”和绿色的电池“内容”。我在Android上研究了很多关于以最佳方式图形化显示电池状态的方法,并得出结论setBounds是最好的方法。因此,我认为简单的自定义ViewonDraw解决方案可能不适合我。我将尝试建议1并回报结果。 - curioustechizen
只是自言自语:即使在布局时ImageView更改了边界,我仍然在setContentView()之后强制设置边界。为什么我指定的新边界没有被遵守?我注意到,setImageDrawable()反过来调用了requestLayout() - 为了缓解这个问题,我将setImageDrawable()代码从displayPortrait()移动到onCreate()中。但还是没有改善。 - curioustechizen
一个更新:我正在尝试一种解决方案,它扩展了View,然后使用graphics.drawBitmap()直接将图像绘制到屏幕上。这可能会稍微复杂,因为我需要决定onMeasure()的适当策略。我希望在赏金到期之前有所进展。 - curioustechizen
我已经将“可工作”的代码提交到Google Code项目托管中。感谢您指引我正确的方向。我使用了自定义的Viewcanvas.drawBitmap()来创建电池图形。我将在原问题的答案中添加更多细节。 - curioustechizen

0
只是猜测,但在RotateActivity中,尝试更改super调用的顺序为:
super.onConfigurationChanged(newConfig); // super called before initialize()
mConfig = newConfig;
initialize();

好的发现!但是它不起作用。我尝试了一些其他的东西,我在我的问题中更新了它们。 - curioustechizen
我其实不太想阅读你的所有代码,但是为什么不在每次重新加载时以与首次创建时相同的方式设置电池图像的大小呢?只需保存该值(可能在instanceState中)并重复该操作即可。 - AJcodez
是的 - 我已经尝试过了。这就是我在我的编辑中提到的。每次我回到纵向方向时,我都会强制设置电池百分比。正如我所提到的,边界被正确更新。只是它们没有“生效”。我甚至尝试在自定义视图的onAttachedToWindow()中强制更新 - 仍然没有用。 - curioustechizen
糟糕..我修改了问题,但没有修改代码本身。稍后会处理。 - curioustechizen

0

我通过使用一个老式的自定义View对象来解决了这个问题。在onDraw()中,我将位图绘制到Canvas上,并根据电池百分比调整大小。在Google项目托管这里上传了工作项目的存档。这个答案帮助了我,因此我已经接受并授予了奖励。

然而,我仍然不确定为什么需要使用自定义View。在我看来,我应该能够将ImageView放置在Layout中,这样也应该可以工作。我只是想不出一种方法告诉布局让我的Drawable保持原样!

此外,这个电池图形最初是打算与其他图形(温度、信号强度等)一起在一个2列表格布局中使用的。因此,这个解决方案对于那些情况的适用性还有待观察。

另外,我的解决方案有一个小bug - 电池在第一次总是显示为空。我会在找到解决方法后把解决方案上传到同一个项目。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接