当App中涉及到布局需要展示大量图片时,你就应该考虑到“图片比例适配“的问题。当图片的宽高规格不同时,你设置展示的ImageView是否可以完好地展示,填充满?也就是说ImagView的比例和图片的比例不匹配,不然的话会导致图片旁会留有空白,这样一系列的组图模块下拉,有的有白边,有的没有,非常影响美观。接下来的自定义控件将可以消除 展示图片有白边的问题,在不对图片进行任何裁剪、拉伸的前提下,最大限度呈现出图片样式!
一. 自定义 按比例展示高度 的控件
以上图的组图页面为例,为了达到更好地显示图片的效果,我们来自定义一个控件,这个控件的宽度填充屏幕,但是高度不确定,根据图片比例具体情况动态的设置高度值。
比如说组图页面展现的图片样式都是这种:图片尺寸为 444 x 183 ,所以比例为 444/183 =2.43 (具体比例根据你APP中需要展示的图片类型而定!)有了比例之后,自定义控件中的 高度 就根据这个 比例 来动态设置。
1.自定义控件 继承帧布局
有了以上规划后,我们可以用一个 帧布局,以它来作为一个容器,让它的宽高严格按照图片的宽高来设置,使得图片填充这个帧布局,图片ImageView无需做任何改动,修改帧布局即可。(这里不需要考虑自定义控件去继承ImageView,太过复杂,帧布局更容易实现,而且在自定义控件和动态填充页面这方面使用非常广)
/**
* 自定义控件, 按照比例来决定布局高度
* Created by gym on 2016/9/15.
*/
public class RatioLayout extends FrameLayout {
private float ratio;
public RatioLayout(Context context) {
super(context);
}
public RatioLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public RatioLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
2.自定义属性,布局编写
xml文件中部分代码
<com.gym.googlemarket.ui.view.RatioLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:ratio="2.43">
<ImageView
android:id="@+id/iv_pic"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/subject_default" />
</com.gym.googlemarket.ui.view.RatioLayout>
attrs.xml 自定义属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="RatioLayout">
<attr name="ratio" format="float"/>
</declare-styleable>
</resources>
如以上布局文件中代码所示,帧布局RatioLayout的宽度填充屏幕,高度随比例而定,也就是会让图片来填充满这个帧布局,所以原先设定的ImageView的属性要修改!
android:layout_height="wrap_content"
android:scaleType="fitXY"
3. 在构造方法中获取 属性值
public RatioLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 获取属性值
// attrs.getAttributeFloatValue("", "ratio", -1);
// 当自定义属性时, 系统会自动生成属性相关id, 此id通过R.styleable来引用
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.RatioLayout);
// id = 属性名_具体属性字段名称 (此id系统自动生成)
ratio = typedArray.getFloat(R.styleable.RatioLayout_ratio, -1);
typedArray.recycle();// 回收typearray, 提高性能
System.out.println("ratio:" + ratio);
}
获取属性值的两种方法:
一.
一般谷歌最常用的是attrs.getAttributeFloatValue("", "ratio", -1);
二.
(1)首先使用 context 的方法获取属性的集合数组。
这里的RatioLayout是在自定义属性里定义的,底层已经将它编译成一个R文件了,就是一个int数组。
(2)调用完方法后,它会返回一个TypedArray类的数组集合,再去get到自定义的属性字段即可。(注意:typedArray.getFloat(R.styleable.RatioLayout_ratio, -1);
中的RatioLayout_ratio id 是系统自动生成:”属性名称_具体字段“)
(3) typeArray 回收,提高性能。
分析完后,明显发现第一种取属性值的方法要简单,所以仅作了解即可。
二. 重写 onMeasure 方法(详解onMeasure )
经过以上步骤,我们已经拿到了 比例值,宽度也确定下拉,现在要根据比例值来设置 自定义控件高度。这就涉及到 尺寸的调整,一个布局,它有三个核心的方法:Measure测量调整它的大小;layout设置它的位置;draw具体绘制。
而我们需要在测量—–Measure里重新修改它的宽高,重写onMeasure方法。方法中需要做的步骤:
a. 获取宽度
b. 根据宽度和比例ratio, 计算控件的高度
c.. 重新测量控件
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 1. 获取宽度
// 2. 根据宽度和比例ratio, 计算控件的高度
// 3. 重新测量控件
//1000000000000000000000111001110
System.out.println("widthMeasureSpec:" + widthMeasureSpec)
int width = MeasureSpec.getSize(widthMeasureSpec);// 获取宽度值
int widthMode = MeasureSpec.getMode(widthMeasureSpec);// 获取宽度模式
int height = MeasureSpec.getSize(heightMeasureSpec);// 获取高度值
int heightMode = MeasureSpec.getMode(heightMeasureSpec);// 获取高度模式
// 宽度确定, 高度不确定, ratio合法, 才计算高度值
if (widthMode == MeasureSpec.EXACTLY
&& heightMode != MeasureSpec.EXACTLY && ratio > 0) {
// 图片宽度 = 控件宽度 - 左侧内边距 - 右侧内边距
int imageWidth = width - getPaddingLeft() - getPaddingRight();
// 图片高度 = 图片宽度/宽高比例
int imageHeight = (int) (imageWidth / ratio + 0.5f);
// 控件高度 = 图片高度 + 上侧内边距 + 下侧内边距
height = imageHeight + getPaddingTop() + getPaddingBottom();
// 根据最新的高度来重新生成heightMeasureSpec(高度模式是确定模式)
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
}
// 按照最新的高度测量控件
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
1. onMeasure 方法中的参数分析
void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法中两个参数widthMeasureSpec、heightMeasureSpec并非是真正的宽高,而是带了一种模式的数值表示。
MeasureSpec类的两个方法,一个是getSize,一个是getMode。首先打印日志查看这个widthMeasureSpec具体值
System.out.println("widthMeasureSpec:" + widthMeasureSpec)
1073742286这么大的值,怎么可能是它的宽度呢?将它转为二进制:
最前面的 ”1“代表的就是模式,后面的数值才是真正的宽度。将111001110 转换成 十进制,是462个像素(如下图蓝线所示)。我的模拟器是 480x800 ,所以这个值是合理的
所以这个widthMeasureSpec值表示的是:模式+宽度值
2. MeasureSpec中模式分析
而MeasureSpec的Mode 模式类型 有三种:
MeasureSpec.AT_MOST:至多模式, 控件有多大显示多大,类似于wrap_content
MeasureSpec.EXACTLY:确定模式, 类似宽高写死成dip,类似 match_parent
MeasureSpec.UNSPECIFIED:未指定模式,不确定宽高,动态计算测量。(举例scrollView,高度决定于它的孩子数量所占的高度)
所以之前转换成那么大的数,最前头的1代表模式 EXACTLY,而根据源码定义 1 << 30
,widthMeasureSpec数值之所以那么大,是因为它左移了30位。
所以真正的宽高值是 将widthMeasureSpec 转换为二进制后的后面几位数,再转换为十进制即可,当然方法中毋须那么复杂,MeasureSpec.getSize(widthMeasureSpec);
即可。
3. 计算控件高度
详解完 onMeasure方法后,则可以开始重新测量布局,步骤a已完成,进行步骤b ,首先 if 判断 当宽度确定(这里的宽度一定要是一个准确值), 高度不确定, ratio合法, 才计算高度值。
if (widthMode == MeasureSpec.EXACTLY
&& heightMode != MeasureSpec.EXACTLY && ratio > 0)
注意:在计算imageView时,一定要考虑 Padding 值,图片宽度 = 控件宽度 - 左侧内边距 - 右侧内边距
根据图片真正的宽度,使用比例计算它的高度,这时我们自定义控件的高度也出来了
控件高度 = 图片高度 + 上侧内边距 + 下侧内边距,最后我们有了真正高度数值后,再获取当前Mode类型,封装好heightMeasureSpec,让父类执行测量的最新高度值即可。
// 按照最新的高度测量控件
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
三.展示效果
总结就是我们就是希望有一个布局,让它完全按照图片的比例来展现原样式,所以我们自定义一个帧布局FrameLayout,让图片去填充这个帧布局,帧布局有多宽多高,展示的图片就有多宽多高。
更改前:
更改后:
可以明显看出,前一张小白边左右多出来的,后者是完全无贴合的,而且并没有裁剪、拉伸图片,按照完美比例展现图片。以下动图就是最后成果,彻底消灭图片留有白边的情况
如果你还是认为这个 图片比例适配的自定义控件没起到什么作用,那我现在把 比例改成1 ,呈现如下,宽高值相同,但是图片的左右两边仍然是无缝衔接,未留有白边!
希望对你有帮助:)