Замена для механизма линейных весов

Фон:

  • Google предлагает избегать использования вложенных взвешенных линейных лотов из-за производительности.
  • Использование вложенных взвешенных linearLayout ужасно для чтения, записи и поддержки.
  • по-прежнему нет хорошей альтернативы для размещения просмотров,% от имеющегося размера. Только решения - это вес и использование OpenGL. В WPF/Silverlight нет даже такого, как "viewBox" для автоматического масштабирования.

Вот почему я решил создать свой собственный макет, который вы расскажете для каждого из своих детей точно, какими должны быть их веса (и окружающие веса) по сравнению с его размером.

Кажется, я преуспел, но почему-то я думаю, что есть некоторые ошибки, которые я не могу отследить.

Одна из ошибок заключается в том, что textView, хотя я даю им много места, он помещает текст сверху, а не в центр. imageViews, с другой стороны, работают очень хорошо. Другая ошибка заключается в том, что если я использую макет (например, frameLayout) внутри моего настраиваемого макета, представления внутри него не будут отображаться (но сам макет).

Пожалуйста, помогите мне разобраться, почему это происходит.

Как использовать: вместо следующего использования линейного макета (я использую длинный XML по назначению, чтобы показать, как мое решение может сократить вещи):

<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>

То, что я делаю, просто (х - это место, где нужно поместить представление в список весов):

<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>

Мой код специального макета:

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;
      }
    }
  }

Ответы

Ответ 1

Теперь есть более приятное решение, чем пользовательский макет, который я сделал:

PercentRelativeLayout

Учебное пособие можно найти здесь и можно найти репо здесь.

Пример кода:

<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/>

или

 <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/>

Интересно, может ли он справиться с проблемами, которые я здесь показал.

Ответ 2

Я принял ваш вызов и попытался создать макет, который вы описываете, в ответ на мои комментарии. Ты прав. Это удивительно сложно. Кроме того, мне нравятся стреляющие домашние мухи. Поэтому я прыгнул на борт и придумал это решение.

  • Расширьте существующие классы макетов, а не создайте свой собственный с нуля. Я пошел с RelativeLayout для начала, но тот же подход может использоваться всеми из них. Это дает вам возможность использовать поведение по умолчанию для этого макета для дочерних представлений, которые вы не хотите манипулировать.

  • Я добавил четыре атрибута в макет, названный top, left, width и height. Мое намерение состояло в том, чтобы имитировать HTML, позволяя такие значения, как "10%", "100 пикселей", "100 дп" и т.д. В это время единственным принятым значением является целое число, представляющее% родительского. "20" = 20% от макета.

  • Для лучшей производительности я разрешаю super.onLayout() выполнять все итерации и обрабатывать только просмотры с помощью пользовательских атрибутов на последнем проходе. Поскольку эти представления будут размещены и масштабированы независимо от братьев и сестер, мы можем перемещать их после того, как все остальное засело.

Вот 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>

Вот мой класс макета.

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 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;
        }
        }
}

Вот пример активности 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>

Вот изображения, которые я использовал для тестирования.

bgenter image description here

Если вы не устанавливаете значение для определенного атрибута, оно будет использоваться по умолчанию. Поэтому, если вы задаете ширину, но не высоту, изображение будет масштабироваться по ширине и wrap_content для высоты.

папка с zipped-проектом.

apk

Я нашел источник ошибки. Проблема в том, что я использовал подсчет дочерних элементов макета, как в индикаторе того, сколько звонков на onLayout он сделает. Это, похоже, не верно в старых версиях Android. Я заметил, что в 2.1 onLayout вызывается только один раз. Поэтому я изменил

if(pass == childCount){

к

if(true){  

и он начал работать как ожидалось.

Я по-прежнему считаю целесообразным настроить макет только после того, как супер сделано. Просто нужно найти лучший способ узнать, когда это возможно.

ИЗМЕНИТЬ

Я не понимал, что вы намерены собирать изображения с точностью до пикселя. Я достиг точности, которую вы ищете, используя переменные точности double float вместо целых чисел. Однако вы не сможете выполнить это, позволяя масштабировать ваши изображения. Когда изображения масштабируются, пиксели добавляются с некоторым интервалом между существующими пикселями. Цвет новых пикселей - это средневзвешенное значение окружающих пикселей. Когда вы масштабируете изображения независимо друг от друга, они не обмениваются информацией. В результате вы всегда будете иметь артефакт в шве. Добавьте к этому результат округления, так как у вас не может быть частичный пиксель, и вы всегда будете иметь допуски на +/- 1 пиксель.

Чтобы убедиться в этом, вы можете выполнить одну и ту же задачу в своем программном обеспечении для редактирования фотографий премиум-класса. Я использую PhotoShop. Используя те же изображения, что и в моем apk, я поместил их в отдельные файлы. Я увеличил их на 168% по вертикали и 127% по горизонтали. Затем я поместил их в файл и попытался выровнять их. Результат был таким же, как и в моем apk.

Чтобы продемонстрировать точность макета, я добавил вторую активность в apk. В этом упражнении я не масштабировал фоновое изображение. Все остальное точно такое же. Результат получается бесшовным.

Я также добавил кнопку, чтобы показать/скрыть оверлейное изображение, а другое - переключиться между действиями.

Я обновил папку apk и zipped на моем диске Google. Вы можете получить их по ссылкам выше.

Ответ 3

После того как вы попробовали свой код, я просто нахожу причину проблем, о которых вы упомянули, и это потому, что в вашем настраиваемом макете вы только макет ребенка правильно, однако вы забыли измерить ваш ребенок правильно, что напрямую повлияет на иерархию чертежа, поэтому просто добавьте нижеприведенный код, и он работает для меня.

    @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

Я предлагаю использовать следующие оптимизации:

<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>

или используйте http://developer.android.com/reference/android/widget/LinearLayout.html#attr_android:weightSum

или используйте TableLayout с layout_weight для строк и столбцов

или используйте GridLayout.