1.开发进度

高仿QQ实现即时通讯项目进度:70%

2.开发日志

Android开发日志

2020-09-12 2.2

​ 实现分类创建群组功能。

2020-09-10 2.1

​ 实现自定义分类创建群组界面。

2020-09-05 2.0

​ 实现群组分组展示、选择好友创建群聊。

2020-09-01 1.9

​ 更换视频播放器。

2020-08-27 1.8

​ 实现图片、视频选择器。

2020-08-25 1.7

​ 修复自定义九宫格展示视频时,视频未加载完成出现透明直接看见手机应用界面的bug。

2020-08-21 1.6

​ 自定义好友动态九宫格图片视频展示功能、图片大图查看功能。

2020-08-16 1.5

​ 实现好友动态点赞、动态评论功能。

2020-08-13 1.4

​ 使用SSM框架实现朋友圈动态后端接口实现(发布动态、获取朋友动态)。

2020-08-5 1.3

​ 实现朋友圈动态界面布局、及发布动态相关界面。

2020-08-2 1.2

​ 实现添加好友、动态列表。

2020-07-28 1.1

​ 集成环信SDK,实现主界面框架、消息列表、好友列表。

2020-07-24 1.0

​ 实现登录注册功能。

3.实现效果

4.说明

自学了Android开发后,突然对QQ的交互体验和设计感兴趣,便想模仿QQ的设计实现一个仿QQ的即时通讯App。在实现过程中根据QQ的界面功能进行模仿开发。因为不知道QQ用了哪些技术,所以在实现的过程中全靠根据QQ的界面和交互动画来进行想象并实现。虽然还有很多不完善的地方,但基本实现了QQ的一些基本功能(为什么不是全部呢?因为QQ功能实在太多,而且我是一个人开发,当做自学完Android原生开发后的一个练手项目。由于本人技术还比较菜,所以有些效果不能完全相同),并且加了一些自己的想法。以下是我实现的一些功能效果图:

登录界面

注册界面

消息界面

联系人界面

动态界面

抽屉栏界面

好友聊天界面

群聊天界面

好友聊天设置界面

群聊天设置界面

删除好友弹窗

解散群聊或退出群聊弹窗

退出登录弹窗

添加好友及通知界面

选择好友创建群聊界面

按分类创建群聊界面

创建群聊界面

二维码生成界面

好友动态界面(1)

好友动态界面(2)

写说说界面

选择说说图片界面

收到消息通知弹窗

收到消息通知

相册界面指纹验证弹窗

我的博客嵌入界面

扫描二维码界面

群公告界面

发表群公告界面

好了,以上就是我实现的一些功能截图。通过这个项目也学到了很多的新东西,并且在实现的过程中也遇到了许多的困难,但是好在我通过不断地学习也成功的解决了这些问题。由于时间问题,项目中还有很多功能未实现,也有许多bug(包括一些不符合我的要求的设计,可能我有一点强迫症和完美主义的原因吧!由于时间关系还没来得及解决或者说暂时还没有想到更好的方案去实现,O(∩_∩)O哈哈~)未解决。等想到更好的方法和方案再去修改吧。总的来说这个项目对我还是有很大提升的。

5.使用到的技术

  1. 前端:Android的原生开发(glide,OKHttp….)、环信SDK3.6.2等
  2. 后端:jdk1.8、Spring、SpringMVC、MyBatis、MySQL5.7、Redis6.0等
  3. 开发工具:Android Studio、IntelliJ IDEA 2019.1 x64、Navicat for MySQL、Postman(接口测试)

6.网友问题答疑

最近B站许多小伙伴私信我要源码,可是因为我用的工具版本比较老旧,还有Android Gradle 插件JCenter 代码库已于 2021 年 3 月 31 日变为只读代码库的原因,所以就不开源了。但是可以提供一下我的思路,以及一些问题的解决方法。

1.消息气泡怎么实现的?

DragMsgView.class

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.LinearInterpolator;

import com.example.administrator.myqq.R;
/**
* Created by Administrator on 2021/5/3.
* 仿qq消息气泡
*/
public class DragMsgView extends AppCompatTextView {

private DragDotView mDragDotView;
private float mWidth, mHeight;
private OnDragListener mDragListener;

public DragMsgView(Context context) {
this(context, null);
}

public DragMsgView(Context context, AttributeSet attrs) {
super(context, attrs);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
mWidth = w;
mHeight = h;
super.onSizeChanged(w, h, oldw, oldh);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
//获得根View
View rootView = getRootView();
//获得触摸位置在全屏所在位置
float mRawX = event.getRawX();
float mRawY = event.getRawY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//子view决定自己处理此事件,并阻止父控件处理这个事件
getParent().requestDisallowInterceptTouchEvent(true);
//获得当前View控件在其整个屏幕上的坐标位置
int[] cLocation = new int[2];
getLocationOnScreen(cLocation);

if(rootView instanceof ViewGroup){
mDragDotView = new DragDotView(getContext());

//设置固定圆和移动圆的圆心坐标
mDragDotView.setDragPoint(cLocation[0] + mWidth / 2, cLocation[1] + mHeight / 2, mRawX, mRawY);

Bitmap bitmap = getBitmapFromView(this);
if(bitmap != null){
mDragDotView.setCacheBitmap(bitmap);
((ViewGroup) rootView).addView(mDragDotView);
setVisibility(INVISIBLE);
}
}
break;
case MotionEvent.ACTION_MOVE:
getParent().requestDisallowInterceptTouchEvent(true);
if(mDragDotView != null){
mDragDotView.move(mRawX, mRawY);
}
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
if(mDragDotView != null){
mDragDotView.up();
}
break;
}
return true;
}

/**
* 将当前view缓存为bitmap,拖动的时候直接绘制此bitmap
* @param view
* @return
*/
public Bitmap getBitmapFromView(View view) {
Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}

public class DragDotView extends View {

// 气泡默认状态--静止
private final int BUBBLE_STATE_DEFAULT = 0;
// 气泡相连
private final int BUBBLE_STATE_CONNECT = 1;
// 气泡分离
private final int BUBBLE_STATE_APART = 2;
// 气泡消失
private final int BUBBLE_STATE_DISMISS = 3;

// 气泡半径
private float mBubbleRadius = dpToPx(10);
// 气泡颜色
private int mBubbleColor;

// 不动气泡的半径
private float mBubbleStillRadius;
// 可动气泡的半径
private float mBubbleMoveRadius;
// 不动气泡的圆心
private PointF mBubStillCenter;
// 可动气泡的圆心
private PointF mBubMoveCenter;

// 气泡的画笔
private Paint mBubblePaint;
// 贝塞尔曲线
private Path mBezierPath;

private Paint mBurstPaint;
private Rect mBurstRect;

// 气泡状态标志
private int mBubbleState = BUBBLE_STATE_DEFAULT;
// 两气泡圆心距离
private float mDist;
// 气泡相连状态最大圆心距离
private float mMaxDist;
// 手指触摸偏移量
private float MOVE_OFFSET;

private Bitmap mCacheBitmap;
// View的宽和高
private float mWidth, mHeight;

// 气泡爆炸的bitmap数组
private Bitmap[] mBurstBitmapArray;
// 是否在执行气泡爆炸动画
private boolean mIsBurstAnimStart = false;
// 当前气泡爆炸图片index
private int mCurDrawableIndex;
// 气泡爆炸的图片id数组
private final int[] mBurstDrawablesArray = {R.drawable.explosion_one, R.drawable.explosion_two
, R.drawable.explosion_three, R.drawable.explosion_four, R.drawable.explosion_five};

public DragDotView(Context context) {
this(context, null);
}

public DragDotView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public DragDotView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}

public DragDotView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs, defStyleAttr);
}

private void init(Context context, AttributeSet attrs, int defStyleAttr){
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DragBubbleView, defStyleAttr, 0);
mBubbleColor = array.getColor(R.styleable.DragBubbleView_bubbleColor, Color.RED);
array.recycle();

// 抗锯齿
mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBubblePaint.setColor(mBubbleColor);
mBubblePaint.setStyle(Paint.Style.FILL);

mBezierPath = new Path();

mBurstPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBurstPaint.setFilterBitmap(true);
mBurstRect = new Rect();
mBurstBitmapArray = new Bitmap[mBurstDrawablesArray.length];
for (int i = 0; i < mBurstDrawablesArray.length; i++) {
// 将气泡爆炸的drawable转为bitmap
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), mBurstDrawablesArray[i]);
mBurstBitmapArray[i] = bitmap;
}
}

@Override
protected void onDraw(Canvas canvas) {
if(mDist < mMaxDist && mBubbleState == BUBBLE_STATE_CONNECT){
// 画静止的气泡
canvas.drawCircle(mBubStillCenter.x, mBubStillCenter.y, mBubbleStillRadius, mBubblePaint);

// 计算控制点坐标,两圆心的中点
int controlX = (int) ((mBubStillCenter.x + mBubMoveCenter.x) / 2);
int controlY = (int) ((mBubStillCenter.y + mBubMoveCenter.y) / 2);

float sin = (mBubMoveCenter.y - mBubStillCenter.y) / mDist;
float cos = (mBubMoveCenter.x - mBubStillCenter.x) / mDist;

// 按照图示的位置,此时移动气泡的y坐标比固定气泡的y坐标小,所以sin是负值,故在使用sin值的时候使用加号计算
// A点
float bubbleStillStartX = mBubStillCenter.x + mBubbleStillRadius * sin;
float bubbleStillStartY = mBubStillCenter.y - mBubbleStillRadius * cos;
// B点
float bubbleMoveStartX = mBubMoveCenter.x + mBubbleMoveRadius * sin;
float bubbleMoveStartY = mBubMoveCenter.y - mBubbleMoveRadius * cos;
// C点
float bubbleMoveEndX = mBubMoveCenter.x - mBubbleMoveRadius * sin;
float bubbleMoveEndY = mBubMoveCenter.y + mBubbleMoveRadius * cos;
// D点
float bubbleStillEndX = mBubStillCenter.x - mBubbleStillRadius * sin;
float bubbleStillEndY = mBubStillCenter.y + mBubbleStillRadius * cos;

mBezierPath.reset();
// 画上半弧
mBezierPath.moveTo(bubbleStillStartX, bubbleStillStartY);
mBezierPath.quadTo(controlX, controlY, bubbleMoveStartX, bubbleMoveStartY);
// 画下半弧
mBezierPath.lineTo(bubbleMoveEndX, bubbleMoveEndY);
mBezierPath.quadTo(controlX, controlY, bubbleStillEndX, bubbleStillEndY);
mBezierPath.close();
canvas.drawPath(mBezierPath, mBubblePaint);
}

// 绘制拖动的view,也就是缓存的bitmap
if (mCacheBitmap != null && mBubbleState != BUBBLE_STATE_DISMISS) {
canvas.drawBitmap(mCacheBitmap,
mBubMoveCenter.x - mWidth / 2,
mBubMoveCenter.y - mHeight / 2,
mBubblePaint);
}

if(mBubbleState == BUBBLE_STATE_DISMISS){
if(mIsBurstAnimStart){
mBurstRect.set((int)(mBubMoveCenter.x - mBubbleMoveRadius), (int)(mBubMoveCenter.y - mBubbleMoveRadius),
(int)(mBubMoveCenter.x + mBubbleMoveRadius), (int)(mBubMoveCenter.y + mBubbleMoveRadius));
canvas.drawBitmap(mBurstBitmapArray[mCurDrawableIndex], null, mBurstRect, mBurstPaint);
}
}
}

/**
* 气泡爆炸动画
*/
private void startBubbleBurstAnim() {
ValueAnimator animator = ValueAnimator.ofInt(0, mBurstDrawablesArray.length);
animator.setInterpolator(new LinearInterpolator());
animator.setDuration(500);
animator.addUpdateListener(animation -> {
mCurDrawableIndex = (int) animator.getAnimatedValue();
invalidate();
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mIsBurstAnimStart = false;
if(mDragListener != null){
mDragListener.onDismiss();
}
}
});
animator.start();
}

/**
* 气泡还原动画
*/
private void startBubbleRestAnim() {
mBubbleStillRadius = mBubbleRadius;
ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(), new PointF(mBubMoveCenter.x, mBubMoveCenter.y), new PointF(mBubStillCenter.x, mBubStillCenter.y));
animator.setDuration(300);
animator.setInterpolator(input -> {
// float factor = 0.4f;
// return (float) (Math.pow(2, -10 * factor) * Math.sin((input - factor / 4) * (2 * Math.PI) / factor) + 1);
float f = 0.571429f;
return (float) (Math.pow(2, -4 * input) * Math.sin((input - f / 4) * (2 * Math.PI) / f) + 1);
});
animator.addUpdateListener(animation -> {
mBubMoveCenter = (PointF) animation.getAnimatedValue();
invalidate();
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mBubbleState = BUBBLE_STATE_DEFAULT;
removeDragView();
if(mDragListener != null){
mDragListener.onRestore();
}
}
});
animator.start();
}

/**
* 设置缓存的bitmap
* @param bitmap
*/
public void setCacheBitmap(Bitmap bitmap){
this.mCacheBitmap = bitmap;
mWidth = mCacheBitmap.getWidth();
mHeight = mCacheBitmap.getHeight();
mBubbleRadius = Math.min(mWidth, mHeight) / 2;
}

/**
* 设置固定圆和移动圆的圆心和半径
* @param stillX 固定圆的X坐标
* @param stillY 固定圆的Y坐标
* @param moveX 移动圆的X坐标
* @param moveY 移动圆的Y坐标
*/
public void setDragPoint(float stillX,
float stillY,
float moveX,
float moveY) {
mBubbleStillRadius = mBubbleRadius;
mBubbleMoveRadius = mBubbleStillRadius;
mMaxDist = mBubbleRadius * 8;
MOVE_OFFSET = mMaxDist / 4;
if(mBubStillCenter == null){
mBubStillCenter = new PointF(stillX, stillY);
}else {
mBubStillCenter.set(stillX, stillY);
}
if(mBubMoveCenter == null){
mBubMoveCenter = new PointF(moveX, moveY);
}else {
mBubMoveCenter.set(moveX, moveY);
}
mBubbleState = BUBBLE_STATE_CONNECT;
invalidate();
}

public void move(float curX, float curY) {
mBubMoveCenter.x = curX;
mBubMoveCenter.y = curY;
// Math.hypot(x, y)为求x平方+y平方的平方根
mDist = (float) Math.hypot(curX - mBubStillCenter.x, curY - mBubStillCenter.y);
if(mBubbleState == BUBBLE_STATE_CONNECT){
if(mDist < mMaxDist - MOVE_OFFSET){
mBubbleStillRadius = mBubbleRadius - mDist / 10;
}else {
mBubbleState = BUBBLE_STATE_APART;
}
}
invalidate();
}

public void up() {
if(mBubbleState == BUBBLE_STATE_CONNECT){
startBubbleRestAnim();
}else if(mBubbleState == BUBBLE_STATE_APART){
if(mDist < 3 * mBubbleRadius){
startBubbleRestAnim();
}else {
mBubbleState = BUBBLE_STATE_DISMISS;
mIsBurstAnimStart = true;
startBubbleBurstAnim();
}
}
invalidate();
}

/**
* 移除dragview
*/
private void removeDragView() {
ViewGroup viewGroup = (ViewGroup) getParent();
viewGroup.removeView(DragDotView.this);
DragMsgView.this.setVisibility(VISIBLE);
}

/**
* 转换 dp 至 px
*
* @param dp dp像素
* @return
*/
protected int dpToPx(float dp) {
DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics();
return (int) (dp * metrics.density + 0.5f);
}
}

public interface OnDragListener{
void onRestore();
void onDismiss();
}

public void setOnDragListener(OnDragListener listener){
this.mDragListener = listener;
}
}

PointEvaluator.class


/**
* Created by Administrator on 2021/5/3.
*/

import android.animation.TypeEvaluator;
import android.graphics.PointF;

public class PointEvaluator implements TypeEvaluator<PointF> {
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
//只要能保证:当fraction=0时返回值为startValue,并且当fraction=1时返回值为endValue,就是一个比较合理的函数
float x = startValue.x + fraction * (endValue.x - startValue.x);
float y = startValue.y + fraction * (endValue.y - startValue.y);
return new PointF(x, y);
}
}

使用方法如下:

  1. xml文件中添加组件

    <com.example.administrator.myqq.bubble.DragMsgView
    android:id="@+id/message_number"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:background="@drawable/bubble_view_shape"
    android:gravity="center"
    android:paddingLeft="7dp"
    android:paddingRight="7dp"
    android:text="99+"
    android:textColor="@android:color/white"
    android:textSize="13sp"
    android:layout_alignParentTop="true"
    android:layout_alignParentRight="true"
    android:layout_marginRight="10dp"
    android:layout_marginEnd="10dp"/>
  2. 设置监听

    private DragMsgView messgeNumber = (DragMsgView) itemView.findViewById(R.id.message_number);


    messgeNumber.setOnDragListener(new DragMsgView.OnDragListener() {
    @Override
    public void onRestore() {
    //气泡恢复原来位置

    }

    @Override
    public void onDismiss() {
    //气泡爆炸消失

    }
    });

2.怎样实现侧滑消息显示删除等按钮?

(待写……)


有啥想问的,欢迎评论区留言!