这篇文章介绍一个仿知乎广告效果。
先上效果图
具体表现在一个recycleView中的某一个item插入一张自己的图。
从图中的效果来看,大致需要几点需求
- 图片应该是一次性加载进来的,而且图片的宽高是根据recycleView等比缩放的
- 图片需要跟随recycleView的滚动而滚动
这时刚好想到一种图片的滚动方式canvas.translate()
这个api起到的作用是移动画布图层.
具体实现
将一张图按宽等比缩放,铺满整个画布。然后根据recycleView的移动来调用canvas.translate()
来不断重绘图片。
下面贴上代码
AdvertisementImageActivity.class
一个recycleView,两个item类型
RecyclerView rv_content;
private static final int LIST_TYPE_AD = 0x11;
private static final int LIST_TYPE_NORMAL = LIST_TYPE_AD + 1;
适配器MyAdapter
class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyHolder> {
@Override
public int getItemViewType(int position) {
if (position == 10) {
return LIST_TYPE_AD;
}
return LIST_TYPE_NORMAL;
}
@Override
public MyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view;
if (viewType == LIST_TYPE_AD) {
view = View.inflate(AdvertisementImageActivity.this, R.layout.item1, null);
} else {
view = View.inflate(AdvertisementImageActivity.this, R.layout.item0, null);
RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
view.setLayoutParams(lp);
}
return new MyHolder(view);
}
@Override
public void onBindViewHolder(MyHolder holder, int position) {
if (position == 10) {
holder.windowImageView.bindRecyclerView(rv_content);
holder.windowImageView.setImageResource(R.drawable.timg2);
} else {
holder.itemView.setBackgroundColor(Color.rgb((int) (Math.random() * 255), (int) (Math.random() * 255), (int) (Math.random() * 255)));
}
}
@Override
public int getItemCount() {
return 20;
}
class MyHolder extends RecyclerView.ViewHolder {
AdvertisementView windowImageView;
MyHolder(View itemView) {
super(itemView);
windowImageView = itemView.findViewById(R.id.wiv);
}
}
}
主要的逻辑代码在AdvertisementView
(继承自view) 中。
AdvertisementView.class
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
realWidth = measureHandle(getSuggestedMinimumWidth(), widthMeasureSpec);
realHeight = measureHandle(getSuggestedMinimumHeight(), heightMeasureSpec);
setMeasuredDimension(realWidth, realHeight);
createBitmap();
}
private int measureHandle(int defaultSize, int measureSpec) {
int result;
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
if (specMode == View.MeasureSpec.EXACTLY || specMode == View.MeasureSpec.AT_MOST) {
result = specSize;
} else {
result = defaultSize;
}
return result;
}
获取view的宽高,本来通过 getWidth()和getHeight()方法也能获取,只不过因为我需要在 onMeasure()中测量广告图的大小,这个时候的getWidth()和getHeight()返回为0,所以通过变量的形式实现。
createBitmap()函数
这个函数干了什么?
- 将广告图按照自定义View的宽度等比缩放
- 设置承载广告图画布的最大偏移量,即第一次出现时最大的偏移量
- 按照比例scale值计算画布的实际偏移量drawableDisY,并重绘
private void createBitmap() {
Resources resources = mContext.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resId, options);
// outWidth是以dp为单位的,需要做一次单位转化
int outWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, options.outWidth, resources.getDisplayMetrics());
int outHeightPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, options.outHeight, resources.getDisplayMetrics());
scale = 1.0f * realWidth / outWidthPx;
int processedWidth = (int) (scale * outWidthPx);
processedHeight = (int) (scale * outHeightPx);
options.inSampleSize = calculateInSampleSize(outWidthPx, outHeightPx, processedWidth, processedHeight);
options.inJustDecodeBounds = false;
Bitmap sourceBitmap = BitmapFactory.decodeResource(resources, resId, options);
Matrix mMatrix = new Matrix();
mMatrix.postScale(scale, scale);
Bitmap targetBitmap = Bitmap.createBitmap(sourceBitmap, 0, 0, sourceBitmap.getWidth(), sourceBitmap.getHeight(), mMatrix, true);
targetDrawable = new BitmapDrawable(resources, targetBitmap);
sourceBitmap.recycle();
maxDistanceY = -targetBitmap.getHeight() + realHeight;
new Handler().post(new Runnable() {
@Override
public void run() {
getLocationInWindow(location);
drawableDisY = (recyclerLocation[1] - location[1]) * scale;
boundTop();
invalidate();
}
});
}
onDraw函数
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (resId == 0) return;
canvas.save();
canvas.translate(0, drawableDisY);
targetDrawable.setBounds(0, 0, realWidth, processedHeight);
targetDrawable.draw(canvas);
canvas.restore();
}
绘制视图,注意要保存画布,在视图绘制结束之后,还原画布。
通过setBounds()绘制广告图的原始大小,注意这里的图片不会受限于父类大小的影响
targetDrawable.setBounds(0, 0, realWidth, processedHeight);
接下来我们的广告图还要跟随recycleView的滚动而滚动。
public void bindRecyclerView(RecyclerView recyclerView) {
if (recyclerView == null || recyclerView.equals(this.recyclerView)) {
return;
}
this.recyclerView = recyclerView;
rvHeight = recyclerView.getLayoutManager().getHeight();
recyclerView.getLocationInWindow(recyclerLocation);
recyclerView.addOnScrollListener(rvScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (getTopDistance() > 0 && getTopDistance() + getHeight() < rvHeight) {
drawableDisY += dy * scale;
boundTop();
invalidate();
}
}
});
}
监听recycleView的滚动距离,动态修改广告图画布的偏移量,这样看上去就能达到文章开头哪有的效果了。
补上剩余代码
//控制画布偏移量
private void boundTop() {
if (drawableDisY > 0) {
drawableDisY = 0;
}
if (drawableDisY < maxDistanceY) {
drawableDisY = maxDistanceY;
}
}
//获取广告图的Y轴距离
private int getTopDistance() {
getLocationInWindow(location);
return location[1] - recyclerLocation[1];
}
以上流程就能满足知乎广告的低配效果
-----------------------优化版本--------------------
低配版本有两个大问题
- 多次滑动之后,会发现滑动距离不准确
- 每次加载广告的时候有明显卡顿效果
关于第一个问题是因为每次加载广告图的时候,都有一个绑定recycleView的操作,而添加滑动监听也会创建多个对象。导致出现异常
recyclerView.addOnScrollListener(rvScrollListener = new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (getTopDistance() > 0 && getTopDistance() + getHeight() < rvHeight) {
drawableDisY += dy * scale;
boundTop();
invalidate();
}
}
});
解决方法也很简单,在绑定之前加一个取消监听的操作即可
private void unbindRecyclerView() {
if (rvScrollListener != null) {
recyclerView.removeOnScrollListener(rvScrollListener);
}
recyclerView = null;
}
关于第二个问题,是因为广告图尺寸较大,放在主线程加载是有可能导致界面卡顿的,那这样干脆加一个辅助类来处理图片
DrawableHelper.java
public class DrawableHelper {
private Context mContext;
private AdvertisementImageView2 mView;
private Drawable targetDrawable;
private float scale;
private ProcessListener listener;
public DrawableHelper(Context mContext, AdvertisementImageView2 mView) {
this.mContext = mContext;
this.mView = mView;
}
public void setProcessListener(ProcessListener listener){
this.listener = listener;
}
public void createDrawable() {
new Thread(new Runnable() {
@Override
public void run() {
int resId = mView.getResourceId();
Resources resources = mContext.getResources();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(resources, resId, options);
// options.outWidth is dp, need do dp -> px
int outWidthPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, options.outWidth, resources.getDisplayMetrics());
int outHeightPx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, options.outHeight, resources.getDisplayMetrics());
scale = 1.0f * mView.getRealWidth() / outWidthPx;
int processedWidth = (int) (scale * outWidthPx);
int processedHeight = (int) (scale * outHeightPx);
options.inSampleSize = calculateInSampleSize(outWidthPx, outHeightPx, processedWidth, processedHeight);
options.inJustDecodeBounds = false;
Bitmap sourceBitmap = BitmapFactory.decodeResource(resources, resId, options);
Matrix mMatrix = new Matrix();
mMatrix.postScale(scale, scale);
Bitmap targetBitmap = Bitmap.createBitmap(sourceBitmap, 0, 0, sourceBitmap.getWidth(), sourceBitmap.getHeight(), mMatrix, true);
targetDrawable = new BitmapDrawable(resources, targetBitmap);
sourceBitmap.recycle();
listener.ProcessFinish(processedWidth, processedHeight);
}
}).start();
}
public Drawable getTargetDrawable() {
return targetDrawable;
}
private int calculateInSampleSize(int sourceWidth, int sourceHeight, int reqWidth, int reqHeight) {
int inSampleSize = 1;
if (sourceWidth > reqWidth || sourceHeight > reqHeight) {
int halfWidth = sourceWidth / 2;
int halfHeight = sourceHeight / 2;
while ((halfWidth / inSampleSize > reqWidth)
&& (halfHeight / inSampleSize > reqHeight)) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public interface ProcessListener {
void ProcessFinish(int width, int height);
}
}
子线程处理图片,接口回调处理结果,自定义View中根据回调结果刷新界面
helper = new DrawableHelper(context, this);
helper.setProcessListener(new DrawableHelper.ProcessListener() {
@Override
public void ProcessFinish(int width, int height) {
rescaleHeight = height;
scale = 1.0f * height / rvHeight;
mMimDisPlayTop = -height + realHeight;
getLocationInWindow(location);
drawableDisY = (rvLocation[1] - location[1]) * scale;
boundTop();
post(new Runnable() {
@Override
public void run() {
invalidate();
}
});
}
});
要注意的是,回调结果也是在子线程中,所以需要post()方法发回UI线程。
相关链接
最后贴上代码地址