展示一波
show.gifgif链接:https://img.haomeiwen.com/i7588686/58365907312b7d23.gif?imageMogr2/auto-orient/strip
绘制需求描述
- 首先选中某个涂鸦礼物后,进入涂鸦礼物绘制模式,可以在屏幕区域内自由的绘制,手指移动的时选中的礼物图片需要摆放在移动的轨迹上。
- 礼物图片以正圆的形式紧密排布,但是当前正圆不能和上一个正圆有相交重叠。
- 涂鸦礼物可以支持切换,切换为另一种礼物后,手指移动绘制的当前选中的礼物图片。
-
支持撤销当前轨迹绘制的礼物,支持清空所有绘制的礼物,关闭涂鸦礼物绘制模式。
diy_示意图.png
方案选择
-
首先想到的是自定义一个view,重写onTouchEvent方法,在该方法中获取用户手指移动的坐标点B,已知上一个礼物的坐标A(手指down下去的坐标就是第一个礼物的坐标),求出A、B两点之间的间距线段AB,此时可以计算出线段AB能放几个礼物正圆(正圆的直径取礼物图片宽或高的小值),在求出每个正圆的圆心坐标,根据这些坐标就能绘制出相应的礼物图片。
坐标计算.png - 另外由于我们的礼物图片是网络图片,所以势必需要下载,所以我们可以用LruCache来缓存下载好的bitmap,还有就是由于用户手指一放上去移动,我们的view就需要把图片绘制上去,但是此时可能我们的图片还未下载完成,所以我们可以参考imageView设置一张占位图,待图片下载成功后把占位图替换即可。
编写对应的实现代码
/**
* 礼物信息实体类
*/
public static class GiftInfo {
public String imageUrl;
public int giftId;
}
/**
* 每一条手势轨迹实体类
*/
public static class PosWrap {
//当前轨迹的礼物bitmap对象
private Bitmap bitmap;
//当前轨迹的礼物信息
public GiftInfo giftInfo;
//当前轨迹的所有礼物的位置区域
private List<RectF> dstList = new ArrayList<>();
//当前轨迹的所有礼物的原始位置区域(做缩放动画时候需要使用)
private List<RectF> originDstList;
public int getGiftSize() {
return dstList.size();
}
}
public class GiftDiyView extends View {
private static final String TAG = "PathGiftView";
//记录按下去的点x轴坐标
private float initialTouchX;
//记录按下去的点y轴坐标
private float initialTouchY;
//滑动阀值,手指移动的距离大于该值便是滑动
private final int touchSlop;
//发送使用的画笔
private Paint sendPaint;
//发送的数据
private SparseArray<PosWrap> sendDataArray;
//序号,用来标识一次touch事件
private int sequenceNumber;
//图片的内存缓存
private final LruCache<String, Bitmap> lruCache;
//占位图
private Bitmap placeholder;
//当前发送方绘制时的图片
private Bitmap currentBitmap;
//当前发送方绘制时的礼物信息
private GiftInfo currentGift;
//当前正在下载图片的集合
private Map<String, Target<Bitmap>> targetMap;
//强制指定的图片的宽度
private int imageWidth;
//强制指定的图片的宽度
private int imageHeight;
public GiftDiyView(Context context) {
this(context, null);
}
public GiftDiyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GiftDiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
sendPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
sendDataArray = new SparseArray<>();
targetMap = new HashMap<>();
imageWidth = dp2px(52);
imageHeight = dp2px(40);
placeholder = ImageUtils.getBitmap(R.drawable.ic_gift_default, imageWidth, imageHeight);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
long max = Runtime.getRuntime().maxMemory();
int cacheSize = (int) (max / 8);
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
}
/**
* 设置当前绘制的礼物
*/
public void setGiftInfo(int giftId, @NonNull String imageUrl) {
if (currentGift != null && giftId == currentGift.giftId) {
return;
}
currentGift = new GiftInfo();
currentGift.giftId = giftId;
currentGift.imageUrl = imageUrl;
Bitmap bitmap;
currentBitmap = (bitmap = lruCache.get(imageUrl)) == null ? placeholder : bitmap;
if (bitmap == null) {
loadBitmap(imageUrl);
}
}
/**
* 加载图片
*/
private void loadBitmap(@NonNull final String imageUrl) {
if (targetMap.containsKey(imageUrl)) {
//该图片正在加载
return;
}
targetMap.put(imageUrl, GlideApp.with(this)
.asBitmap()
.load(imageUrl)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.override(imageWidth, imageHeight)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
targetMap.remove(imageUrl);
lruCache.put(imageUrl, resource);
//通知图片加载成功
notifyBitmapUpdate(imageUrl);
loggerD("onResourceReady: 图片加载成功 imageUrl: " + imageUrl);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
targetMap.remove(imageUrl);
loggerE("onLoadFailed: 图片加载失败");
}
}));
}
/**
* 图片加载成功,需要从placeholder切换为正确的图片
*/
private void notifyBitmapUpdate(@NonNull String imageUrl) {
Bitmap bitmap = Objects.requireNonNull(lruCache.get(imageUrl), "请检查代码逻辑,通知bitmap更新中不允许bitmap为null");
boolean invalidate = false;
for (int i = 0, size = sendDataArray.size(); i < size; i++) {
PosWrap posWrap = sendDataArray.valueAt(i);
if (updateBitmap(posWrap, imageUrl, bitmap)) {
invalidate = true;
}
}
if (currentGift != null && imageUrl.equals(currentGift.imageUrl)) {
currentBitmap = bitmap;
}
loggerD("notifyBitmapUpdate invalidate: " + invalidate);
if (invalidate) {
invalidate();
}
}
/**
* 判断该图片是否需要更新
*/
private boolean updateBitmap(@NonNull PosWrap posWrap, @NonNull String imageUrl, @NonNull Bitmap bitmap) {
if (posWrap.giftInfo.imageUrl.equals(imageUrl) && posWrap.bitmap == placeholder) {
posWrap.bitmap = bitmap;
return true;
}
return false;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (currentGift == null) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录down的坐标
initialTouchX = getValidX(event);
initialTouchY = getValidY(event);
//序号++
sequenceNumber++;
initPosWrap(initialTouchX, initialTouchY);
break;
case MotionEvent.ACTION_MOVE:
float x = getValidX(event);
float y = getValidY(event);
float dx = x - initialTouchX;
float dy = y - initialTouchY;
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
if (canDraw() && addPosByCircle(x, y)) {
invalidate();
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
break;
}
return true;
}
/**
* x轴坐标边界修正
*/
private float getValidX(@NonNull MotionEvent event) {
float x = event.getX();
int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
float min, max;
if (x < (min = value)) {
return min;
} else if (x > (max = getWidth() - value)) {
return max;
} else {
return x;
}
}
/**
* y轴坐标边界修正
*/
private float getValidY(@NonNull MotionEvent event) {
float y = event.getY();
int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
float min, max;
if (y < (min = value)) {
return min;
} else if (y > (max = getHeight() - value)) {
return max;
} else {
return y;
}
}
/**
* 通知当前礼物有变化
*/
private void notifyGiftCountChange() {
if (giftDiyListener != null) {
giftDiyListener.onGiftChange(getAllGiftCount());
}
}
/**
* 判断是否能绘制礼物
*/
private boolean canDraw() {
return giftDiyListener == null || giftDiyListener.canDraw(currentGift.giftId);
}
/**
* 添加当前轨迹的第一个点
*/
private void initPosWrap(float x, float y) {
if (sendDataArray.get(sequenceNumber) != null) {
throw new UnsupportedOperationException("posWrapArray中不应该有当前sequenceNumber:" + sequenceNumber);
}
if (canDraw()) {
//初始化当前轨迹的礼物信息对象
PosWrap posWrap = new PosWrap();
posWrap.bitmap = currentBitmap;
posWrap.giftInfo = currentGift;
//添加当前轨迹的第一个点的范围
posWrap.dstList.add(getDrawDstByPos(x, y));
sendDataArray.put(sequenceNumber, posWrap);
invalidate();
notifyGiftCountChange();
}
}
private RectF getDrawDstByPos(float x, float y) {
return getDstByPos(currentBitmap, x, y);
}
/**
* 根据图片的大小和坐标点计算出礼物的显示范围
*/
private RectF getDstByPos(Bitmap bitmap, float x, float y) {
float left = x - (bitmap.getWidth() >> 1);
float top = y - (bitmap.getHeight() >> 1);
float right = left + bitmap.getWidth();
float bottom = top + bitmap.getHeight();
return new RectF(left, top, right, bottom);
}
/**
* 缩放成正方形
*/
private RectF zoomSquare(@NonNull RectF src) {
float sub = src.width() - src.height();
if (sub > 0) {
return new RectF(src.left + sub / 2, src.top, src.right - sub / 2, src.bottom);
} else {
return new RectF(src.left, src.top + Math.abs(sub) / 2, src.right, src.bottom - Math.abs(sub) / 2);
}
}
/**
* 根据正方形范围判断该坐标是否在圆内
*/
private boolean inCircle(@NonNull RectF square, float x, float y) {
float dx = Math.abs(square.centerX() - x);
float dy = Math.abs(square.centerY() - y);
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) < square.width() / 2;
}
private boolean addPosByCircle(float endX, float endY) {
//获取当前轨迹信息
PosWrap posWrap = sendDataArray.get(sequenceNumber);
//根据上一个点缩放出一个正方形
RectF square = zoomSquare(posWrap.dstList.get(posWrap.dstList.size() - 1));
//根据该正方形判断当前move的坐标是否在圆的范围内
if (inCircle(square, endX, endY)) {
return false;
}
/*
* 以下根据上一个的中心点做出的圆的圆心坐标,做一条线段到当前坐标
* 然后根据该线段的长度,计算可以放的下多少个圆,并把每个圆的圆心坐标计算出来,
* 就得出了每个礼物的坐标点
*/
float startX = square.centerX();
float startY = square.centerY();
float dx = endX - startX;
float dy = endY - startY;
int numX = dx > 0 ? 1 : -1;
int numY = dy > 0 ? 1 : -1;
float absDx = Math.abs(dx);
float absDy = Math.abs(dy);
int size = (int) (Math.sqrt(Math.pow(absDx, 2) + Math.pow(absDy, 2)) / square.width());
if (size <= 0) {
return false;
}
float fx, fy;
if (absDx == 0) {
fx = 0;
fy = 1;
} else if (absDy == 0) {
fx = 1;
fy = 0;
} else {
double degree = Math.atan(absDx / absDy);
fx = (float) Math.sin(degree);
fy = (float) Math.cos(degree);
}
boolean invalid = false;
for (int i = 1; i <= size; i++) {
float x = startX + fx * square.width() * i * numX;
float y = startY + fy * square.width() * i * numY;
if (!canDraw()) {
break;
}
invalid = true;
posWrap.dstList.add(getDrawDstByPos(x, y));
}
if (invalid) {
notifyGiftCountChange();
}
return invalid;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/*
* 绘制手势轨迹
*/
for (int i = 0, size = sendDataArray.size(); i < size; i++) {
PosWrap posWrap = sendDataArray.valueAt(i);
Bitmap bitmap = posWrap.bitmap;
for (RectF dst : posWrap.dstList) {
canvas.drawBitmap(bitmap, null, dst, sendPaint);
}
}
}
}
其实以上代码就是做了方案选择当中的事情,接下来还有收到他人的涂鸦礼物,需要动画展示,动画要求如下:
按照用户绘制顺序逐个出现
缩放出现0%-100% ,用时200ms,
每张礼物图片出现间隔60ms
礼物至多绘制100个,至少绘制10个,所以动画最长时间为6200ms,最短时间为800ms
全部礼物图片出现后,停留1500ms后,缩放100%-200%并渐隐100%-0%消失,用时300ms。
完整代码如下:
public class GiftDiyView extends View {
private static final String TAG = "PathGiftView";
//记录按下去的点x轴坐标
private float initialTouchX;
//记录按下去的点y轴坐标
private float initialTouchY;
//滑动阀值,手指移动的距离大于该值便是滑动
private final int touchSlop;
//发送使用的画笔
private Paint sendPaint;
//接收使用的画笔
private Paint receivePaint;
//发送的数据
private SparseArray<PosWrap> sendDataArray;
//序号,用来标识一次touch事件
private int sequenceNumber;
//图片的内存缓存
private final LruCache<String, Bitmap> lruCache;
//占位图
private Bitmap placeholder;
//当前发送方绘制时的图片
private Bitmap currentBitmap;
//当前发送方绘制时的礼物信息
private GiftInfo currentGift;
//当前正在下载图片的集合
private Map<String, Target<Bitmap>> targetMap;
//强制指定的图片的宽度
private int imageWidth;
//强制指定的图片的宽度
private int imageHeight;
//接收方的数据
private List<PosWrap> receiveDataList;
//接收到数据后的动画是否正在执行
private boolean isAnimatorRunning;
//接收方的离场动画
private ValueAnimator exitAnimator;
//接收方的进场动画集合
private List<ValueAnimator> enterScaleAnimators;
//礼物监听器
private GiftDiyListener giftDiyListener;
public GiftDiyView(Context context) {
this(context, null);
}
public GiftDiyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public GiftDiyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
sendPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
receivePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
sendDataArray = new SparseArray<>();
targetMap = new HashMap<>();
imageWidth = dp2px(52);
imageHeight = dp2px(40);
placeholder = ImageUtils.getBitmap(R.drawable.ic_gift_default, imageWidth, imageHeight);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
long max = Runtime.getRuntime().maxMemory();
int cacheSize = (int) (max / 8);
lruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};
receiveDataList = new ArrayList<>();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width, height;
width = widthMode == MeasureSpec.EXACTLY ? widthSize : ViewGroup.LayoutParams.MATCH_PARENT;
height = heightMode == MeasureSpec.EXACTLY ? heightSize : ViewGroup.LayoutParams.MATCH_PARENT;
setMeasuredDimension(width, height);
}
/**
* @return 当前绘制的礼物id
*/
public int getCurrentGiftId() {
return currentGift == null ? 0 : currentGift.giftId;
}
/**
* 设置当前绘制的礼物
*/
public void setGiftInfo(int giftId, @NonNull String imageUrl) {
if (currentGift != null && giftId == currentGift.giftId) {
return;
}
currentGift = new GiftInfo();
currentGift.giftId = giftId;
currentGift.imageUrl = imageUrl;
Bitmap bitmap;
currentBitmap = (bitmap = lruCache.get(imageUrl)) == null ? placeholder : bitmap;
if (bitmap == null) {
loadBitmap(imageUrl);
}
}
/**
* 删除当前绘制的礼物
*/
public void deleteCurrentGift() {
int size = sendDataArray.size();
if (size > 0) {
sendDataArray.removeAt(size - 1);
invalidate();
notifyGiftCountChange();
}
}
/**
* 清空绘制的礼物
*/
public void clearMoveGift() {
if (sendDataArray.size() > 0) {
clearPosWrapArray();
invalidate();
notifyGiftCountChange();
}
}
/**
* 释放绘制方
*/
private void releaseTouchDraw() {
clearPosWrapArray();
currentGift = null;
currentBitmap = null;
}
/**
* 释放绘制方后重新回调onDraw
*/
public void closeTouchDraw() {
releaseTouchDraw();
invalidate();
}
/**
* 释放所有
*/
public void release() {
releaseTouchDraw();
clearExitAnimator();
handler.removeCallbacksAndMessages(null);
clearEnterScaleAnimators();
isAnimatorRunning = false;
receiveDataList.clear();
clearTarget();
lruCache.evictAll();
giftDiyListener = null;
placeholder = null;
}
/**
* 获取绘制的所有礼物总数
*/
public int getTotalGiftCount() {
int totalCount = 0;
for (int i = 0, size = sendDataArray.size(); i < size; i++) {
totalCount += sendDataArray.valueAt(i).getGiftSize();
}
return totalCount;
}
/**
* 获取每一种礼物的绘制数量
*/
public SparseIntArray getAllGiftCount() {
SparseIntArray giftCounts = new SparseIntArray();
for (int i = 0, size = sendDataArray.size(); i < size; i++) {
PosWrap posWrap = sendDataArray.valueAt(i);
int newCount = posWrap.getGiftSize() + giftCounts.get(posWrap.giftInfo.giftId);
giftCounts.put(posWrap.giftInfo.giftId, newCount);
}
return giftCounts;
}
/**
* 获取某个礼物的绘制数量
*/
public int getGiftCountByGiftId(long giftId) {
int count = 0;
for (int i = 0, size = sendDataArray.size(); i < size; i++) {
PosWrap posWrap = sendDataArray.valueAt(i);
if (posWrap.giftInfo.giftId == giftId) {
count += posWrap.getGiftSize();
}
}
return count;
}
/**
* 获取绘制的礼物坐标信息等
*/
public List<PosWrap> getPosData() {
int size = sendDataArray.size();
List<PosWrap> list = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
PosWrap posWrap = sendDataArray.valueAt(i);
list.add(posWrap);
}
return list;
}
/**
* 接收到他人的diy礼物
*/
public void receiveDiyGift(@NonNull MessageData data) {
if (isAnimatorRunning || data.list.isEmpty() || data.list.get(0).posList.isEmpty() || !receiveDataList.isEmpty()) {
return;
}
isAnimatorRunning = true;
sendNextMessage(data, 0L);
}
public void setGiftDiyListener(@Nullable GiftDiyListener giftDiyListener) {
this.giftDiyListener = giftDiyListener;
}
/**
* 清空发送方的数据
*/
private void clearPosWrapArray() {
sequenceNumber = 0;
sendDataArray.clear();
}
/**
* 取消接收方的进场动画
*/
private void clearEnterScaleAnimators() {
if (enterScaleAnimators != null) {
Iterator<ValueAnimator> iterator = enterScaleAnimators.iterator();
while (iterator.hasNext()) {
ValueAnimator animator = iterator.next();
animator.removeAllUpdateListeners();
animator.removeAllListeners();
animator.cancel();
iterator.remove();
}
enterScaleAnimators = null;
}
}
/**
* 取消接收方的离场动画
*/
private void clearExitAnimator() {
if (exitAnimator != null) {
exitAnimator.removeAllUpdateListeners();
exitAnimator.removeAllListeners();
exitAnimator.cancel();
exitAnimator = null;
}
}
/**
* 接收方展示下一个礼物图片的消息
*/
private static final int NEXT_MSG = 1;
/**
* 接收方所有礼物退场的消息
*/
private static final int EXIT_MSG = 2;
private Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == NEXT_MSG) {
MessageData data = (MessageData) msg.obj;
//当前轨迹要展示的礼物信息
ReceivePosWrap wrap = data.list.get(data.index);
PosWrap receiveData;
if (receiveDataList.size() <= data.index) {
receiveData = new PosWrap();
receiveData.giftInfo = wrap.giftInfo;
//该集合用来保存礼物的原始位置和范围,退出动画的时候需要用到原始的范围
receiveData.originDstList = new ArrayList<>();
Bitmap bitmap;
receiveData.bitmap = (bitmap = lruCache.get(receiveData.giftInfo.imageUrl)) == null ? placeholder : bitmap;
if (bitmap == null) {
loadBitmap(receiveData.giftInfo.imageUrl);
}
receiveDataList.add(receiveData);
} else {
receiveData = receiveDataList.get(data.index);
}
//坐标点等比例转换
float[] pos = coverPos(wrap.posList.get(data.dstIndex), data.width, data.height);
//根据坐标点确认礼物的显示范围
RectF dst = getDstByPos(receiveData.bitmap, pos[0], pos[1]);
boolean isLast = false;
//获取当前轨迹的下个礼物
data.dstIndex++;
if (data.dstIndex >= wrap.posList.size()) {
//当前轨迹已没有礼物
data.dstIndex = 0;
//获取下一个轨迹
data.index++;
if (data.index >= data.list.size()) {
//没有下一个轨迹,证明没用任何礼物了
isLast = true;
}
}
//添加该礼物的范围
receiveData.dstList.add(dst);
//添加该礼物的原始范围
receiveData.originDstList.add(new RectF(dst));
//开启该礼物的进场动画
startEnterScaleAnimator(dst, isLast);
if (isLast) {
//结束
loggerD("结束了");
} else {
//60ms后进行下一个礼物的进场
sendNextMessage(data, 60L);
}
} else if (msg.what == EXIT_MSG) {
//所有的礼物一起执行离场动画
startExitAnimator();
}
}
};
private Activity findActivity(Context context) {
if (context instanceof Activity) {
return (Activity) context;
} else if (context instanceof ContextWrapper) {
return findActivity(((ContextWrapper) context).getBaseContext());
} else {
return null;
}
}
private int getValidScreenHeight() {
Activity activity = findActivity(getContext());
int navBarHeight;
if (activity == null) {
navBarHeight = 0;
} else {
navBarHeight = BarUtils.isNavBarVisible(activity) ? BarUtils.getNavBarHeight() : 0;
}
return ScreenUtils.getScreenHeight() - navBarHeight;
}
public int[] getViewSize() {
int w = getWidth() == 0 ? ScreenUtils.getScreenWidth() : getWidth();
int h = getHeight() == 0 ? getValidScreenHeight() : getHeight();
return new int[]{w, h};
}
/**
* 坐标点比例转换
* 根据发送方的宽高结合接收方自己的宽高对发送方发来的坐标点进行等比例转换
*/
private float[] coverPos(float[] pos, int width, int height) {
int[] size = getViewSize();
float x = pos[0] * size[0] / width;
float y = pos[1] * size[1] / height;
return new float[]{x, y};
}
private void sendNextMessage(@NonNull MessageData data, long delayMillis) {
Message message = handler.obtainMessage(NEXT_MSG);
message.obj = data;
handler.sendMessageDelayed(message, delayMillis);
}
/**
* 每个礼物执行的进场动画(缩放出现0%-100%)
* 每个礼物出现间隔60ms
*/
private void startEnterScaleAnimator(final RectF dst, final boolean isLast) {
if (enterScaleAnimators == null) {
enterScaleAnimators = new ArrayList<>();
}
final float left = dst.left;
final float top = dst.top;
final float right = dst.right;
final float bottom = dst.bottom;
final float centerX = dst.centerX();
final float centerY = dst.centerY();
ValueAnimator animator;
enterScaleAnimators.add(animator = ValueAnimator.ofFloat(0f, 1f).setDuration(200L));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
dst.set(animatedValue * left + (1 - animatedValue) * centerX,
animatedValue * top + (1 - animatedValue) * centerY,
animatedValue * right + (1 - animatedValue) * centerX,
animatedValue * bottom + (1 - animatedValue) * centerY);
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
enterScaleAnimators.remove(((ValueAnimator) animation));
loggerD("enterScaleAnimators.size: " + enterScaleAnimators.size());
if (isLast) {
//若是最后一个礼物,停留1500ms后需执行离场动画
handler.sendEmptyMessageDelayed(EXIT_MSG, 1500L);
}
}
});
animator.start();
}
/**
* 所有的礼物一起执行离场动画(缩放100%-200%并渐隐100%-0%消失)
*/
private void startExitAnimator() {
exitAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(300L);
exitAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
for (PosWrap receiveData : receiveDataList) {
for (int i = 0, size = receiveData.dstList.size(); i < size; i++) {
RectF originDst = receiveData.originDstList.get(i);
RectF dst = receiveData.dstList.get(i);
float targetLeft = originDst.left - originDst.width() / 2;
float targetTop = originDst.top - originDst.height() / 2;
float targetRight = originDst.right + originDst.width() / 2;
float targetBottom = originDst.bottom + originDst.height() / 2;
dst.set(animatedValue * targetLeft + (1 - animatedValue) * originDst.left,
animatedValue * targetTop + (1 - animatedValue) * originDst.top,
animatedValue * targetRight + (1 - animatedValue) * originDst.right,
animatedValue * targetBottom + (1 - animatedValue) * originDst.bottom);
}
}
receivePaint.setAlpha((int) ((1 - animatedValue) * 255));
invalidate();
}
});
exitAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
exitAnimator = null;
receiveDataList.clear();
receivePaint.setAlpha(255);
isAnimatorRunning = false;
//通知接收方礼物展示结束
if (giftDiyListener != null) {
giftDiyListener.receiveDiyGiftEnd();
}
}
});
exitAnimator.start();
}
/**
* 加载图片
*/
private void loadBitmap(@NonNull final String imageUrl) {
if (targetMap.containsKey(imageUrl)) {
//该图片正在加载
return;
}
targetMap.put(imageUrl, GlideApp.with(this)
.asBitmap()
.load(imageUrl)
.diskCacheStrategy(DiskCacheStrategy.DATA)
.override(imageWidth, imageHeight)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
targetMap.remove(imageUrl);
lruCache.put(imageUrl, resource);
//通知图片加载成功
notifyBitmapUpdate(imageUrl);
loggerD("onResourceReady: 图片加载成功 imageUrl: " + imageUrl);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
targetMap.remove(imageUrl);
loggerE("onLoadFailed: 图片加载失败");
}
}));
}
/**
* 图片加载成功,需要从placeholder切换为正确的图片
*/
private void notifyBitmapUpdate(@NonNull String imageUrl) {
Bitmap bitmap = Objects.requireNonNull(lruCache.get(imageUrl), "请检查代码逻辑,通知bitmap更新中不允许bitmap为null");
boolean invalidate = false;
for (int i = 0, size = sendDataArray.size(); i < size; i++) {
PosWrap posWrap = sendDataArray.valueAt(i);
if (updateBitmap(posWrap, imageUrl, bitmap)) {
invalidate = true;
}
}
for (PosWrap posWrap : receiveDataList) {
if (updateBitmap(posWrap, imageUrl, bitmap)) {
invalidate = true;
}
}
if (currentGift != null && imageUrl.equals(currentGift.imageUrl)) {
currentBitmap = bitmap;
}
loggerD("notifyBitmapUpdate invalidate: " + invalidate);
if (invalidate) {
invalidate();
}
}
/**
* 判断该图片是否需要更新
*/
private boolean updateBitmap(@NonNull PosWrap posWrap, @NonNull String imageUrl, @NonNull Bitmap bitmap) {
if (posWrap.giftInfo.imageUrl.equals(imageUrl) && posWrap.bitmap == placeholder) {
posWrap.bitmap = bitmap;
return true;
}
return false;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (currentGift == null) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//记录down的坐标
initialTouchX = getValidX(event);
initialTouchY = getValidY(event);
//序号++
sequenceNumber++;
initPosWrap(initialTouchX, initialTouchY);
break;
case MotionEvent.ACTION_MOVE:
float x = getValidX(event);
float y = getValidY(event);
float dx = x - initialTouchX;
float dy = y - initialTouchY;
if (Math.abs(dx) > touchSlop || Math.abs(dy) > touchSlop) {
if (canDraw() && addPosByCircle(x, y)) {
invalidate();
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
break;
}
return true;
}
/**
* x轴坐标边界修正
*/
private float getValidX(@NonNull MotionEvent event) {
float x = event.getX();
int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
float min, max;
if (x < (min = value)) {
return min;
} else if (x > (max = getWidth() - value)) {
return max;
} else {
return x;
}
}
/**
* y轴坐标边界修正
*/
private float getValidY(@NonNull MotionEvent event) {
float y = event.getY();
int value = Math.min(currentBitmap.getWidth(), currentBitmap.getHeight()) >> 1;
float min, max;
if (y < (min = value)) {
return min;
} else if (y > (max = getHeight() - value)) {
return max;
} else {
return y;
}
}
/**
* 通知当前礼物有变化
*/
private void notifyGiftCountChange() {
if (giftDiyListener != null) {
giftDiyListener.onGiftChange(getAllGiftCount());
}
}
/**
* 判断是否能绘制礼物
*/
private boolean canDraw() {
return giftDiyListener == null || giftDiyListener.canDraw(currentGift.giftId);
}
/**
* 添加当前轨迹的第一个点
*/
private void initPosWrap(float x, float y) {
if (sendDataArray.get(sequenceNumber) != null) {
throw new UnsupportedOperationException("posWrapArray中不应该有当前sequenceNumber:" + sequenceNumber);
}
if (canDraw()) {
//初始化当前轨迹的礼物信息对象
PosWrap posWrap = new PosWrap();
posWrap.bitmap = currentBitmap;
posWrap.giftInfo = currentGift;
//添加当前轨迹的第一个点的范围
posWrap.dstList.add(getDrawDstByPos(x, y));
sendDataArray.put(sequenceNumber, posWrap);
invalidate();
notifyGiftCountChange();
}
}
private RectF getDrawDstByPos(float x, float y) {
return getDstByPos(currentBitmap, x, y);
}
/**
* 根据图片的大小和坐标点计算出礼物的显示范围
*/
private RectF getDstByPos(Bitmap bitmap, float x, float y) {
float left = x - (bitmap.getWidth() >> 1);
float top = y - (bitmap.getHeight() >> 1);
float right = left + bitmap.getWidth();
float bottom = top + bitmap.getHeight();
return new RectF(left, top, right, bottom);
}
/**
* 缩放成正方形
*/
private RectF zoomSquare(@NonNull RectF src) {
float sub = src.width() - src.height();
if (sub > 0) {
return new RectF(src.left + sub / 2, src.top, src.right - sub / 2, src.bottom);
} else {
return new RectF(src.left, src.top + Math.abs(sub) / 2, src.right, src.bottom - Math.abs(sub) / 2);
}
}
/**
* 根据正方形范围判断该坐标是否在圆内
*/
private boolean inCircle(@NonNull RectF square, float x, float y) {
float dx = Math.abs(square.centerX() - x);
float dy = Math.abs(square.centerY() - y);
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) < square.width() / 2;
}
private boolean addPosByCircle(float endX, float endY) {
//获取当前轨迹信息
PosWrap posWrap = sendDataArray.get(sequenceNumber);
//根据上一个点缩放出一个正方形
RectF square = zoomSquare(posWrap.dstList.get(posWrap.dstList.size() - 1));
//根据该正方形判断当前move的坐标是否在圆的范围内
if (inCircle(square, endX, endY)) {
return false;
}
/*
* 以下根据上一个的中心点做出的圆的圆心坐标,做一条线段到当前坐标
* 然后根据该线段的长度,计算可以放的下多少个圆,并把每个圆的圆心坐标计算出来,
* 就得出了每个礼物的坐标点
*/
float startX = square.centerX();
float startY = square.centerY();
float dx = endX - startX;
float dy = endY - startY;
int numX = dx > 0 ? 1 : -1;
int numY = dy > 0 ? 1 : -1;
float absDx = Math.abs(dx);
float absDy = Math.abs(dy);
int size = (int) (Math.sqrt(Math.pow(absDx, 2) + Math.pow(absDy, 2)) / square.width());
if (size <= 0) {
return false;
}
float fx, fy;
if (absDx == 0) {
fx = 0;
fy = 1;
} else if (absDy == 0) {
fx = 1;
fy = 0;
} else {
double degree = Math.atan(absDx / absDy);
fx = (float) Math.sin(degree);
fy = (float) Math.cos(degree);
}
boolean invalid = false;
for (int i = 1; i <= size; i++) {
float x = startX + fx * square.width() * i * numX;
float y = startY + fy * square.width() * i * numY;
if (!canDraw()) {
break;
}
invalid = true;
posWrap.dstList.add(getDrawDstByPos(x, y));
}
if (invalid) {
notifyGiftCountChange();
}
return invalid;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/*
* 绘制接收方的礼物
*/
for (PosWrap receiveData : receiveDataList) {
Bitmap bitmap = receiveData.bitmap;
for (RectF dst : receiveData.dstList) {
canvas.drawBitmap(bitmap, null, dst, receivePaint);
}
}
/*
* 绘制手势轨迹
* 因SparseArray是按照key进行升序排序的,而我们的sequenceNumber只进行++处理,
* 所以可以和ArrayList一样保证遍历的时候是按照添加时的顺序进行遍历
*/
for (int i = 0, size = sendDataArray.size(); i < size; i++) {
PosWrap posWrap = sendDataArray.valueAt(i);
Bitmap bitmap = posWrap.bitmap;
for (RectF dst : posWrap.dstList) {
canvas.drawBitmap(bitmap, null, dst, sendPaint);
}
}
}
/**
* 取消加载图片的任务
*/
private void clearTarget() {
Iterator<Map.Entry<String, Target<Bitmap>>> iterator = targetMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Target<Bitmap>> entry = iterator.next();
GlideApp.with(this).clear(entry.getValue());
iterator.remove();
}
}
private int dp2px(float dpVal) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
dpVal, getResources().getDisplayMetrics());
}
private static void loggerE(String message) {
if (BaseApplication.isDebug()) {
Log.e(TAG, message);
}
}
private static void loggerD(String message) {
if (BaseApplication.isDebug()) {
Log.d(TAG, message);
}
}
public interface GiftDiyListener {
void onGiftChange(@NonNull SparseIntArray giftCounts);
boolean canDraw(int giftId);
void receiveDiyGiftEnd();
}
public static class PosWrap {
private Bitmap bitmap;
public GiftInfo giftInfo;
private List<RectF> dstList = new ArrayList<>();
private List<RectF> originDstList;
public int getGiftSize() {
return dstList.size();
}
public float[] covertPos() {
int size = dstList.size();
if (size == 0) {
return null;
}
float[] pos = new float[size * 2];
int index = 0;
for (RectF rectF : dstList) {
pos[index] = rectF.centerX();
index++;
pos[index] = rectF.centerY();
index++;
}
return pos;
}
}
public static class GiftInfo {
public String imageUrl;
public int giftId;
}
public static class MessageData {
private int index;
private int dstIndex;
public List<ReceivePosWrap> list = new ArrayList<>();
public int width;
public int height;
}
public static class ReceivePosWrap {
public GiftInfo giftInfo = new GiftInfo();
public List<float[]> posList = new ArrayList<>();
}
}
网友评论