本文描述了如何实现该需求的思路,代码可能不通用,但是该思路应该可以解决很多类似的需求…
因为我的需要还是比较复杂的(主要来自于上半部分的排列规则),所以直接 extends 了 ViewGroup 来 layout 这些子 View,然后用数据填充这些布局,然后创建分享 Bitmap。
对自定义View(我们就叫它 DynamicShareView 好了),它关心的是如何拿到上半部分的 View 和下半部分的 View,所以这里定义一个 Adapter 接口:
public interface Adapter {
/**
* 获取封面个数
*/
int getCoverCount();
/**
* 获取封面 View
*/
View getImage(ViewGroup viewGroup, int position);
/**
* 获取底部 View
*/
View getBottom(ViewGroup viewGroup);
}
对调用者来说,我们只需要调用 DynamicShareView
实例的 setAdapter
方法,然后将一个实现了 Adapter 接口的对象
传给它,接着 DynamicShareView
会将这些 View
挨个添加到进来,如下:
public void setAdapter(@NonNull Adapter adapter) {
mAdapter = adapter;
removeAllViews(); // 移除所有子View
// 添加图片
int imageCount = Math.min(mAdapter.getCoverCount(), MAX_COUNT);
for (int index = 0; index < imageCount; index++) {
View image = mAdapter.getImage(this, index);
addView(image);
mImages.add(image);
}
// 添加底部
View bottomLayout = mAdapter.getBottom(this);
mBottomLayout = bottomLayout;
addView(bottomLayout, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
}
这步完成之后,才是重头戏,我们需要自己来 measure
内部的子 View
,计算出 DynamicShareView
的宽高。
这里的实现,emmmmmmmmm,复杂度和产品的需求成正比。栗如,我这里的复杂度主要来自上半部分的排列规则:
1.一张封面时,封面的宽高等于屏幕的宽度
2.两张封面时,封面的宽度等于屏幕的宽度,高度为屏幕宽度的一半,上下排列
3.三张封面时,封面的高度等于屏幕宽度的一半,前两张图片占一行,均分;第三张图片独占一行
4.// 省略…
怎么破,这里,我定义了一个 Rule 接口:
public interface Rule {
void measureChildren(int screenWidth, int screenHeight, int spacing, View... children);
void layoutChildren(int screenWidth, int screenHeight, int spacing, View... children);
}
然后定义5个子类来测量和布局,如下图:
Rule1Impl 对应只有 1 个封面的情况,Rule2Impl 对应有 2 个封面的情况,以此类推。BottomRule 负责下半部分子 View
的测量和布局。当然,如果你喜欢,也可以写成一坨代码来 work
。我们先来看下,onMeasure
方法长咋样吧。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
mActualWidth = widthSize; // 宽度等于屏幕宽度
switch (mImages.size()) {
case 1:
mRule1.measureChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0));
break;
case 2:
mRule2.measureChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0),
mImages.get(1));
break;
case 3:
mRule3.measureChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0),
mImages.get(1), mImages.get(2));
break;
case 4:
mRule4.measureChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0),
mImages.get(1), mImages.get(2), mImages.get(3));
break;
}
// 测量底部高度
mBottomRule.measureChildren(mActualWidth, mActualHeight, mSpacing, mBottomLayout);
// 获得实际高度
mActualHeight = mActualWidth + mBottomLayout.getMeasuredHeight();
// 设置 ViewGroup 尺寸
setMeasuredDimension(mActualWidth,
heightMode == MeasureSpec.EXACTLY ? heightSize : mActualHeight);
}
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mImages == null || mImages.isEmpty()) return;
switch (mImages.size()) {
case 1:
mRule1.layoutChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0));
break;
case 2:
mRule2.layoutChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0),
mImages.get(1));
break;
case 3:
mRule3.layoutChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0), mImages.get(1),
mImages.get(2));
break;
case 4:
mRule4.layoutChildren(mActualWidth, mActualHeight, mSpacing, mImages.get(0), mImages.get(1),
mImages.get(2), mImages.get(3));
break;
}
mBottomRule.layoutChildren(mActualWidth, mActualHeight, mSpacing, mBottomLayout);
}
看上去是不是很干净呢23333!
这里和上半部分不同,不需要根据业务动态排列子 View,所以使用一个 xxxx.xml 来布局,如图:
NOTE:这里的根布局,没有写
layout_height=match_parent
,因为下半部分的高度实际上应由内部的子View
高度来决定!这也就是说,我们进行下半部分测量的时候,我们需要自己计算下半部分的高度到底是多少,这样,才能得到DynamicShareView
的高度数值!那么问题来了,如何测量layout_heigh=wrap_content
的View
的高度呢?看代码:
public class BottomRule extends AbsRule {
public BottomRule(Context context) {
super(context);
}
@Override
public void measureChildren(int screenWidth, int screenHeight, int spacing, View... children) {
measureManually2(children[0], screenWidth);
}
@Override
public void layoutChildren(int screenWidth, int screenHeight, int spacing, View... children) {
children[0].layout(0, screenWidth, screenWidth, screenHeight);
}
}
public abstract class AbsRule implements Rule {
private Context mContext;
public AbsRule(@NonNull Context context) {
mContext = context;
}
protected void measureManually2(View target, int widthSize) {
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(widthSize, View.MeasureSpec.EXACTLY);
int heightMeasureSpec =
View.MeasureSpec.makeMeasureSpec((1 << 30) - 1, View.MeasureSpec.AT_MOST);
target.measure(widthMeasureSpec, heightMeasureSpec);
}
}
看到 measureManually2
这个方法,我想机智的童鞋已经豁然开朗了。至于为什么可以用这种方式测量出 layout_width=wrap_content
的 View
的高度,答案在 《Android开发艺术探索》一书中的 自定义View
一章。
上面讲过只要实现 Adapter
这个接口就可以了,然而实际上,能否成功生成图片的关键也在这里,稍微不注意,就会陷入异步问题的深坑中。
主流方案一般是用 Picasso、Glide
这样的图片加载库,这里,我使用的是 Glide
。那直接 Glide.with().load().into ...
不就万事大吉了嘛!nonono,不是这样滴,因为 Glide
加载图片的过程是异步的,也就是说 ImageView
可能已经全部添加到 DynamicShareView
了,onMeasure、layout 的步骤也完成了,但图片还未从网络、磁盘加载完,ImageView
里的 Bitmap
是不存在的 。这时,我们再这么做:
public void share(View target) {
// 测量 DynamicShareView 的大小
measureManually(target, ScreenUtil.getScreenWidth(mContext),
ScreenUtil.getScreenWidth(mContext) + DensityUtil.dip2px(mContext, 175.0F));
// 布局 DynamicShareView
target.layout(0, 0, target.getMeasuredWidth(), target.getMeasuredHeight());
// 生成 Bitmap
Bitmap bitmap = generateBitmap(target, target.getMeasuredWidth(), target.getMeasuredHeight());
// 将 Bitmap 保存为临时文件
try {
saveBitmap(bitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
mContext.startActivity(createShareIntent());
}
生成的 Bitmap
很可能是没有图片的。因为这些图片都是需要 Glide
去远程图片服务器加载,解析后才能得到的。而我们并不知道加载这些图片需要多久,甚至都没有等待这些加载工作完成,就直接填充数据到 DynamicShareView
上,然后满怀期待地生成 Bitmap
了…
简单来说,就是在知道图片全部加载完成之后,我们再生成最后的分享图,关键就是等!
这里我使用了一个计数器,每当 Glide
加载完成,拿到 Bitmap
后,我就让计数器+1,当计数器等于需要加载的图片个数时,回调。如下:
Glide.with(viewGroup.getContext())
.load(mImages.get(position).getImageUrl())
.asBitmap()
.into(new SimpleTarget<Bitmap>() {
@Override public void onResourceReady(Bitmap resource,
GlideAnimation<? super Bitmap> glideAnimation) {
ivCover.setImageBitmap(resource);
mloadImageCount++;
if (mloadImageCount == getCoverCount() + 1) {
mOnImageLoadedListener.onImageLoaded();
}
}
});
外部构造 Adapter 实例时,传入一个 OnImageLoadedListener 即可。
// MyAdapter 的签名
public MyAdapter(@NonNull Context context, List<Image> images, @NonNull Bottom bottom,
@NonNull OnImageLoadedListener onImageLoadedListener) {
}