Replacement for the linearLayout weights mechanism

2019-03-11 01:29发布

Background:

  • Google suggests to avoid using nested weighted linearLayouts because of performance.
  • using nested weighted linearLayout is awful to read, write and maintain.
  • there is still no good alternative for putting views that are % of the available size. Only solutions are weights and using OpenGL. There isn't even something like the "viewBox" shown on WPF/Silverlight to auto-scale things.

This is why I've decided to create my own layout which you tell for each of its children exactly what should be their weights (and surrounding weights) compared to its size.

It seems I've succeeded , but for some reason I think there are some bugs which I can't track down.

One of the bugs is that textView, even though I give a lot of space for it, it puts the text on the top instead of in the center. imageViews on the other hand work very well. Another bug is that if I use a layout (for example a frameLayout) inside my customized layout, views within it won't be shown (but the layout itself will).

Please help me figure out why it occurs.

How to use: instead of the next usage of linear layout (I use a long XML on purpose, to show how my solution can shorten things):

<LinearLayout 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" android:orientation="vertical">

  <View android:layout_width="wrap_content" android:layout_height="0px"
    android:layout_weight="1" />

  <LinearLayout android:layout_width="match_parent"
    android:layout_height="0px" android:layout_weight="1"
    android:orientation="horizontal">

    <View android:layout_width="0px" android:layout_height="wrap_content"
      android:layout_weight="1" />

    <TextView android:layout_width="0px" android:layout_weight="1"
      android:layout_height="match_parent" android:text="@string/hello_world"
      android:background="#ffff0000" android:gravity="center"
      android:textSize="20dp" android:textColor="#ff000000" />

    <View android:layout_width="0px" android:layout_height="wrap_content"
      android:layout_weight="1" />

  </LinearLayout>
  <View android:layout_width="wrap_content" android:layout_height="0px"
    android:layout_weight="1" />
</LinearLayout>

What I do is simply (the x is where to put the view itself in the weights list):

<com.example.weightedlayouttest.WeightedLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res/com.example.weightedlayouttest"
  xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
  android:layout_height="match_parent" tools:context=".MainActivity">

  <TextView android:layout_width="0px" android:layout_height="0px"
    app:horizontalWeights="1,1x,1" app:verticalWeights="1,1x,1"
    android:text="@string/hello_world" android:background="#ffff0000"
    android:gravity="center" android:textSize="20dp" android:textColor="#ff000000" />

</com.example.weightedlayouttest.WeightedLayout>

My code of the special layout is:

public class WeightedLayout extends ViewGroup
  {
  @Override
  protected WeightedLayout.LayoutParams generateDefaultLayoutParams()
    {
    return new WeightedLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
    }

  @Override
  public WeightedLayout.LayoutParams generateLayoutParams(final AttributeSet attrs)
    {
    return new WeightedLayout.LayoutParams(getContext(),attrs);
    }

  @Override
  protected ViewGroup.LayoutParams generateLayoutParams(final android.view.ViewGroup.LayoutParams p)
    {
    return new WeightedLayout.LayoutParams(p.width,p.height);
    }

  @Override
  protected boolean checkLayoutParams(final android.view.ViewGroup.LayoutParams p)
    {
    final boolean isCorrectInstance=p instanceof WeightedLayout.LayoutParams;
    return isCorrectInstance;
    }

  public WeightedLayout(final Context context)
    {
    super(context);
    }

  public WeightedLayout(final Context context,final AttributeSet attrs)
    {
    super(context,attrs);
    }

  public WeightedLayout(final Context context,final AttributeSet attrs,final int defStyle)
    {
    super(context,attrs,defStyle);
    }

  @Override
  protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
    {
    for(int i=0;i<this.getChildCount();++i)
      {
      final View v=getChildAt(i);
      final WeightedLayout.LayoutParams layoutParams=(WeightedLayout.LayoutParams)v.getLayoutParams();
      //
      final int availableWidth=r-l;
      final int totalHorizontalWeights=layoutParams.getLeftHorizontalWeight()+layoutParams.getViewHorizontalWeight()+layoutParams.getRightHorizontalWeight();
      final int left=l+layoutParams.getLeftHorizontalWeight()*availableWidth/totalHorizontalWeights;
      final int right=r-layoutParams.getRightHorizontalWeight()*availableWidth/totalHorizontalWeights;
      //
      final int availableHeight=b-t;
      final int totalVerticalWeights=layoutParams.getTopVerticalWeight()+layoutParams.getViewVerticalWeight()+layoutParams.getBottomVerticalWeight();
      final int top=t+layoutParams.getTopVerticalWeight()*availableHeight/totalVerticalWeights;
      final int bottom=b-layoutParams.getBottomVerticalWeight()*availableHeight/totalVerticalWeights;
      //
      v.layout(left+getPaddingLeft(),top+getPaddingTop(),right+getPaddingRight(),bottom+getPaddingBottom());
      }
    }

  // ///////////////
  // LayoutParams //
  // ///////////////
  public static class LayoutParams extends ViewGroup.LayoutParams
    {
    int _leftHorizontalWeight =0,_rightHorizontalWeight=0,_viewHorizontalWeight=0;
    int _topVerticalWeight    =0,_bottomVerticalWeight=0,_viewVerticalWeight=0;

    public LayoutParams(final Context context,final AttributeSet attrs)
      {
      super(context,attrs);
      final TypedArray arr=context.obtainStyledAttributes(attrs,R.styleable.WeightedLayout_LayoutParams);
        {
        final String horizontalWeights=arr.getString(R.styleable.WeightedLayout_LayoutParams_horizontalWeights);
        //
        // handle horizontal weight:
        //
        final String[] words=horizontalWeights.split(",");
        boolean foundViewHorizontalWeight=false;
        int weight;
        for(final String word : words)
          {
          final int viewWeightIndex=word.lastIndexOf('x');
          if(viewWeightIndex>=0)
            {
            if(foundViewHorizontalWeight)
              throw new IllegalArgumentException("found more than one weights for the current view");
            weight=Integer.parseInt(word.substring(0,viewWeightIndex));
            setViewHorizontalWeight(weight);
            foundViewHorizontalWeight=true;
            }
          else
            {
            weight=Integer.parseInt(word);
            if(weight<0)
              throw new IllegalArgumentException("found negative weight:"+weight);
            if(foundViewHorizontalWeight)
              _rightHorizontalWeight+=weight;
            else _leftHorizontalWeight+=weight;
            }
          }
        if(!foundViewHorizontalWeight)
          throw new IllegalArgumentException("couldn't find any weight for the current view. mark it with 'x' next to the weight value");
        }
        //
        // handle vertical weight:
        //
        {
        final String verticalWeights=arr.getString(R.styleable.WeightedLayout_LayoutParams_verticalWeights);
        final String[] words=verticalWeights.split(",");
        boolean foundViewVerticalWeight=false;
        int weight;
        for(final String word : words)
          {
          final int viewWeightIndex=word.lastIndexOf('x');
          if(viewWeightIndex>=0)
            {
            if(foundViewVerticalWeight)
              throw new IllegalArgumentException("found more than one weights for the current view");
            weight=Integer.parseInt(word.substring(0,viewWeightIndex));
            setViewVerticalWeight(weight);
            foundViewVerticalWeight=true;
            }
          else
            {
            weight=Integer.parseInt(word);
            if(weight<0)
              throw new IllegalArgumentException("found negative weight:"+weight);
            if(foundViewVerticalWeight)
              _bottomVerticalWeight+=weight;
            else _topVerticalWeight+=weight;
            }
          }
        if(!foundViewVerticalWeight)
          throw new IllegalArgumentException("couldn't find any weight for the current view. mark it with 'x' next to the weight value");
        }
      //
      arr.recycle();
      }

    public LayoutParams(final int width,final int height)
      {
      super(width,height);
      }

    public LayoutParams(final ViewGroup.LayoutParams source)
      {
      super(source);
      }

    public int getLeftHorizontalWeight()
      {
      return _leftHorizontalWeight;
      }

    public void setLeftHorizontalWeight(final int leftHorizontalWeight)
      {
      _leftHorizontalWeight=leftHorizontalWeight;
      }

    public int getRightHorizontalWeight()
      {
      return _rightHorizontalWeight;
      }

    public void setRightHorizontalWeight(final int rightHorizontalWeight)
      {
      if(rightHorizontalWeight<0)
        throw new IllegalArgumentException("negative weight :"+rightHorizontalWeight);
      _rightHorizontalWeight=rightHorizontalWeight;
      }

    public int getViewHorizontalWeight()
      {
      return _viewHorizontalWeight;
      }

    public void setViewHorizontalWeight(final int viewHorizontalWeight)
      {
      if(viewHorizontalWeight<0)
        throw new IllegalArgumentException("negative weight:"+viewHorizontalWeight);
      _viewHorizontalWeight=viewHorizontalWeight;
      }

    public int getTopVerticalWeight()
      {
      return _topVerticalWeight;
      }

    public void setTopVerticalWeight(final int topVerticalWeight)
      {
      if(topVerticalWeight<0)
        throw new IllegalArgumentException("negative weight :"+topVerticalWeight);
      _topVerticalWeight=topVerticalWeight;
      }

    public int getBottomVerticalWeight()
      {
      return _bottomVerticalWeight;
      }

    public void setBottomVerticalWeight(final int bottomVerticalWeight)
      {
      if(bottomVerticalWeight<0)
        throw new IllegalArgumentException("negative weight :"+bottomVerticalWeight);
      _bottomVerticalWeight=bottomVerticalWeight;
      }

    public int getViewVerticalWeight()
      {
      return _viewVerticalWeight;
      }

    public void setViewVerticalWeight(final int viewVerticalWeight)
      {
      if(viewVerticalWeight<0)
        throw new IllegalArgumentException("negative weight :"+viewVerticalWeight);
      _viewVerticalWeight=viewVerticalWeight;
      }
    }
  }

4条回答
干净又极端
2楼-- · 2019-03-11 01:47

I accepted your challenge and attempted to create the layout you describe in response to my comment. You are right. It is surprisingly difficult to accomplish. Besides that, I do like shooting house flies. So I jumped on board and came up with this solution.

  1. Extend the existing layout classes rather than creating your own from scratch. I went with RelativeLayout to start with but the same approach can be used by all of them. This gives you the ability to use the default behavior for that layout on child views that you don't want to manipulate.

  2. I added four attributes to the layout called top, left, width and height. My intention was to mimic HTML by allowing values such as "10%", "100px", "100dp" etc.. At this time the only value accepted is an integer representing the % of parent. "20" = 20% of the layout.

  3. For better performance I allow the super.onLayout() to execute through all of it's iterations and only manipulate the views with the custom attributes on it's last pass. Since these views will be positioned and scaled independently of the siblings we can move them after everything else has settled.

Here is atts.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="HtmlStyleLayout">
        <attr name="top" format="integer"/>
        <attr name="left" format="integer"/>
        <attr name="height" format="integer"/>
        <attr name="width" format="integer"/>

    </declare-styleable>
</resources>

Here is my layout class.

package com.example.helpso;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;


public class HtmlStyleLayout extends RelativeLayout{

    private int pass =0;
    @Override
      protected HtmlStyleLayout.LayoutParams generateDefaultLayoutParams()
        {
        return new HtmlStyleLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT,RelativeLayout.LayoutParams.WRAP_CONTENT);
        }

      @Override
      public HtmlStyleLayout.LayoutParams generateLayoutParams(final AttributeSet attrs)
        {
        return new HtmlStyleLayout.LayoutParams(getContext(),attrs);
        }

      @Override
      protected RelativeLayout.LayoutParams generateLayoutParams(final android.view.ViewGroup.LayoutParams p)
        {
        return new HtmlStyleLayout.LayoutParams(p.width,p.height);
        }

      @Override
      protected boolean checkLayoutParams(final android.view.ViewGroup.LayoutParams p)
        {
        final boolean isCorrectInstance=p instanceof HtmlStyleLayout.LayoutParams;
        return isCorrectInstance;
        }

    public HtmlStyleLayout(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    public void setScaleType(View v){
        try{
            ((ImageView) v).setScaleType (ImageView.ScaleType.FIT_XY);
        }catch (Exception e){
            // The view is not an ImageView 
        }
    }


    @Override
      protected void onLayout(final boolean changed,final int l,final int t,final int r,final int b)
        {
        super.onLayout(changed, l, t, r, b);           //Let the parent layout do it's thing


        pass++;                                        // After the last pass of
        final int childCount = this.getChildCount();   // the parent layout
        if(true){                        // we do our thing


            for(int i=0;i<childCount;++i)
              {
              final View v=getChildAt(i);
              final HtmlStyleLayout.LayoutParams params = (HtmlStyleLayout.LayoutParams)v.getLayoutParams();

              int newTop = v.getTop();                 // set the default value
              int newLeft = v.getLeft();               // of these to the value
              int newBottom = v.getBottom();           // set by super.onLayout() 
              int newRight= v.getRight();             
              boolean viewChanged = false;

              if(params.getTop() >= 0){
                  newTop = ( (int) ((b-t) * (params.getTop() * .01))  );
                  viewChanged = true;
              }

              if(params.getLeft() >= 0){
                  newLeft = ( (int) ((r-l) * (params.getLeft() * .01))  );
                  viewChanged = true;
              }

              if(params.getHeight() > 0){
                  newBottom = ( (int) ((int) newTop + ((b-t) * (params.getHeight() * .01)))  );
                  setScaleType(v);                        // set the scale type to fitxy
                  viewChanged = true;
              }else{
                  newBottom = (newTop + (v.getBottom() - v.getTop()));
                  Log.i("heightElse","v.getBottom()=" +
                          Integer.toString(v.getBottom())
                          + " v.getTop=" +
                          Integer.toString(v.getTop()));
              }

              if(params.getWidth() > 0){
                  newRight = ( (int) ((int) newLeft + ((r-l) * (params.getWidth() * .01)))  );
                  setScaleType(v);
                  viewChanged = true;
              }else{
                  newRight = (newLeft + (v.getRight() - v.getLeft()));
              }

                // only call layout() if we changed something
                if(viewChanged)
                    Log.i("SizeLocation",
                            Integer.toString(i) + ": "
                            + Integer.toString(newLeft) + ", "
                            + Integer.toString(newTop) + ", "
                            + Integer.toString(newRight) + ", "
                            + Integer.toString(newBottom));
                v.layout(newLeft, newTop, newRight, newBottom);
              }





            pass = 0;                                 // reset the parent pass counter
        }
        }


     public  class LayoutParams extends RelativeLayout.LayoutParams
        {

        private int top, left, width, height;
        public LayoutParams(final Context context, final AttributeSet atts) {
            super(context, atts);
            TypedArray a = context.obtainStyledAttributes(atts, R.styleable.HtmlStyleLayout);
            top =  a.getInt(R.styleable.HtmlStyleLayout_top , -1);
            left = a.getInt(R.styleable.HtmlStyleLayout_left, -1);
            width = a.getInt(R.styleable.HtmlStyleLayout_width, -1);
            height = a.getInt(R.styleable.HtmlStyleLayout_height, -1);
            a.recycle();


        }
        public LayoutParams(int w, int h) {
            super(w,h);
            Log.d("lp","2");
        }
        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
            Log.d("lp","3");
        }
        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
            Log.d("lp","4");
        }
        public int getTop(){
            return top;
        }
        public int getLeft(){
            return left;
        }
        public int getWidth(){
            return width;
        }
        public int getHeight(){
            return height;
        }
        }
}

Here is an example activity xml

<com.example.helpso.HtmlStyleLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:html="http://schemas.android.com/apk/res/com.example.helpso"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <ImageView
        android:id="@+id/imageView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentTop="true"
        android:scaleType="fitXY"
        android:src="@drawable/bg" />

    <ImageView
        android:id="@+id/imageView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/overlay"
        html:height="10"
        html:left="13"
        html:top="18"
        html:width="23" />

</com.example.helpso.HtmlStyleLayout>

Here are the images I used for testing.

bg enter image description here

If you do not set a value for a particular attribute it's default will be used. So if you set width but not height the image will scale in width and wrap_content for height.

Zipped project folder.

apk

I found the source of the bug. The problem is that I was using the layout's child count as in indicator of how many calls to onLayout it will make. This doesn't seem to hold true in older versions of android. I noticed in 2.1 onLayout is only called once. So I changed

if(pass == childCount){

to

if(true){  

and it started working as expected.

I still thinks it's beneficial to adjust the layout only after the super is done. Just need to find a better way to know when that is.

EDIT

I didn't realize that your intention was to patch together images with pixel by pixel precision. I achieved the precision you are looking for by using double float precision variables instead of integers. However, you will not be able accomplish this while allowing your images to scale. When an images is scaled up pixels are added at some interval between the existing pixels. The color of the new pixels are some weighted average of the surrounding pixels. When you scale the images independently of each other they don't share any information. The result is that you will always have some artifact at the seam. Add to that the result of rounding since you can't have a partial pixel and you will always have a +/-1 pixel tolerance.

To verify this you can attempt the same task in your premium photo editing software. I use PhotoShop. Using the same images as in my apk, I placed them in seperate files. I scaled them both by 168% vertically and 127% horizontally. I then placed them in a file together and attempted to align them. The result was exactly the same as is seen in my apk.

To demonstrate the accuracy of the layout, I added a second activity to my apk. On this activity I did not scale the background image. Everything else is exactly the same. The result is seamless.

I also added a button to show/hide the overlay image and one to switch between the activities.

I updated both the apk and the zipped project folder on my google drive. You can get them by the links above.

查看更多
▲ chillily
3楼-- · 2019-03-11 01:47

After trying your code, I just find the reason of the problems you mentioned, and it is because in your customed layout, you only layout the child properly, however you forgot to measure your child properly, which will directly affect the drawing hierarchy, so simply add the below code, and it works for me.

    @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec)-this.getPaddingRight()-this.getPaddingRight();
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);

    int heightSize = MeasureSpec.getSize(heightMeasureSpec)-this.getPaddingTop()-this.getPaddingBottom();
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    if(heightMode == MeasureSpec.UNSPECIFIED || widthMode == MeasureSpec.UNSPECIFIED)
        throw new IllegalArgumentException("the layout must have a exact size");

    for (int i = 0; i < this.getChildCount(); ++i) {
        View child = this.getChildAt(i);
        LayoutParams lp = (LayoutParams)child.getLayoutParams();
        int width = lp._viewHorizontalWeight * widthSize/(lp._leftHorizontalWeight+lp._rightHorizontalWeight+lp._viewHorizontalWeight);
        int height =  lp._viewVerticalWeight * heightSize/(lp._topVerticalWeight+lp._bottomVerticalWeight+lp._viewVerticalWeight);
        child.measure(width | MeasureSpec.EXACTLY,  height | MeasureSpec.EXACTLY);
    }

    this.setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));

}
查看更多
霸刀☆藐视天下
4楼-- · 2019-03-11 01:54

Now there is a nicer solution than the custom layout I've made:

PercentRelativeLayout

Tutorial can be found here and a repo can be found here.

Example code:

<android.support.percent.PercentRelativeLayout
         xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
     <ImageView
         app:layout_widthPercent="50%"
         app:layout_heightPercent="50%"
         app:layout_marginTopPercent="25%"
         app:layout_marginLeftPercent="25%"/>
 </android.support.percent.PercentFrameLayout/>

or:

 <android.support.percent.PercentFrameLayout
         xmlns:android="http://schemas.android.com/apk/res/android"
         xmlns:app="http://schemas.android.com/apk/res-auto"
         android:layout_width="match_parent"
         android:layout_height="match_parent"/>
     <ImageView
         app:layout_widthPercent="50%"
         app:layout_heightPercent="50%"
         app:layout_marginTopPercent="25%"
         app:layout_marginLeftPercent="25%"/>
 </android.support.percent.PercentFrameLayout/>

I wonder though if it can handle the issues I've shown here.

查看更多
小情绪 Triste *
5楼-- · 2019-03-11 01:55

I propose to use following optimizations:

<FrameLayout 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" android:gravity="center">


    <TextView android:layout_width="wrap_content"
      android:layout_height="wrap_content" android:text="@string/hello_world"
      android:background="#ffff0000" android:gravity="center"
      android:textSize="20dp" android:textColor="#ff000000" />

</FrameLayout>

or use http://developer.android.com/reference/android/widget/LinearLayout.html#attr_android:weightSum

or use TableLayout with layout_weight for rows and columns

or use GridLayout.

查看更多
登录 后发表回答