Nowadays, it is very common to add chat in most apps, while wechat and QQ are the ancestors of the communication field. If a product manager is thinking about doing chat design, most will refer to it.

Often you will hear, you see micro letter and QQ are so do, you come so, although the psychology has ten thousand not happy, but who call we are a programmer who has pursuit.

Therefore, the requirement of the product is to achieve a group profile picture similar to wechat. Similar to the following

Multiple merger

As a programmer, the first thing you do is evaluate your workload. In the eyes of products, it is to combine pictures together. Is there any difficulty? So the hours you work determine what you can do

Scheme analysis:

Plan 1: Write the layout directly, and then load different numbers of pictures according to different layouts. The common image loading scheme is asynchronous loading, so that when loading, the image will be merged into a flash. Since the current image frames are cached, the second time is much better.

Advantages: Fast to implement

Disadvantages: very low, not a forced grid programmer’s approach, and the effect is not good.

Option 2: Create a custom control and download all images asynchronously. Add a counter to the control to ensure that all images are displayed synchronously once they have been downloaded.

Advantages: Moderate difficulty

Disadvantages: poor scalability, which day the product wants to change a synthetic scheme

Scheme 3, or use the original control, group image after merging to generate a new image, the original after the cache. Abstract the merge algorithm into an interface.

Advantages: Easy to expand, better experience

Cons: Extra time

Of course, as aspiring programmers, we should consider implementing plan 3 and benefit some of our product-afflicted fellow programmers.

Next, LET me talk about the main ideas and key code. In fact, the overall idea is relatively simple, can be summarized with a flow chart.

Merge diagram load logic

First, we know that the input parameter to the application should be an ImageView control, a list of urls. There is also a merge callback function for customizing merge methods.

public void displayImages(
    final List<String> urls,
    final ImageView imageView, 
    final MergeCallBack mergeCallBack
)Copy the code

The idea is that we need to generate a new key from the urls that will be used to cache the merged image, which can be loaded directly from the cache next time. After all, merging avatars is a time-consuming operation

    public String getNewUrlByList(List<String> urls, String mark) {
        StringBuilder sb = new StringBuilder();
        for (String url : urls) {
            sb.append(url + mark);
        }

        return sb.toString();
    }Copy the code

Here is a simple concatenation of all urls and then MD5.

Caching is the most critical step, which involves the caching of individual linked images and the caching of merged images. For a caching system, a single image is treated the same as multiple images, each key corresponds to a cache object. It’s just that the rules for key are slightly different.

The cache scheme is also a secondary cache implemented by the common DiskLruCache and MemoryLruCache implementations to keep the cache efficient. (For Lru algorithm, it is simply the Least Recently Used, i.e. the principle of recent use. Please refer to Baidu for details.)

Let’s look at the core of displayImages, which is to look for the in-memory cache, then look for the disk cache, and if there’s none, then synchronize to find all the individual images

public void displayImages(final List<String> urls, final ImageView imageView, final MergeCallBack mergeCallBack, final int dstWidth, Final int dstHeight) {if (urls = = null | | urls. The size () < = 0) {throw new IllegalArgumentException (" url can't be empty "); } if (mergeCallBack == null) {throw new IllegalArgumentException("mergeCallBack cannot be empty "); } final String url = getNewUrlByList(urls, mergeCallBack.getMark()); imageView.setTag(IMG_URL, url); Bitmap = loadFromMemory(url); if (bitmap ! = null) { LogUtil.e(Tag, "displayImages this is from Memory"); imageView.setImageBitmap(bitmap); return; } try {// Load bitmap from disk = loadFromDiskCache(url, dstWidth, dstHeight); if (bitmap ! = null) { LogUtil.e(Tag, "displayImages this is from Disk"); imageView.setImageBitmap(bitmap); return; } } catch (Exception e) { e.printStackTrace(); } / / set a default figure bitmap = BitmapFactory. DecodeResource (mContext. GetResources (), R.d. Rawable ic_launcher_round); imageView.setImageBitmap(bitmap); LogUtil.e(Tag, "displayImages this is from default"); // Start a new thread to load all images synchronously. Returns if the load is successful. Runnable loadBitmapTask = new Runnable() { @Override public void run() { ArrayList<Bitmap> bitmaps = loadBitMaps(urls, dstWidth, dstHeight); if (bitmaps ! = null && bitmaps.size() > 0) { Result result; if (mergeCallBack ! = null) { Bitmap mergeBitmap = mergeCallBack.merge(bitmaps, mContext, imageView); If (urls.size() == bitmaps.size()) {// Add cache try {saveDru(url, mergeBitmap); } catch (IOException e) { e.printStackTrace(); } } else { LogUtil.e(Tag, "size change. so can not save"); } LogUtil.e(Tag, "displayImages this is from Merge"); result = new Result(mergeBitmap, url, imageView); } else { result = new Result(bitmaps.get(0), url, imageView); } Message msg = mMainHandler.obtainMessage(MESSAGE_SEND_RESULT, result); msg.sendToTarget(); }}}; threadPoolExecutor.execute(loadBitmapTask); }Copy the code

If loading from cache fails, we start a thread to perform the avatar merge. So head merge is a synchronous operation, and we need to get the object that we want to merge our heads with, so how do we get that? Let’s keep looking at the code

private ArrayList<Bitmap> loadBitMaps(List<String> urls, int dstWidth, int dstHeight) { ArrayList<Bitmap> bitmaps = new ArrayList<>(); For (String url: urls) {// Synchronize all images Bitmap Bitmap = loadBitMap(url, dstWidth, dstHeight); if (bitmap ! = null) { bitmaps.add(bitmap); } } return bitmaps; }Copy the code

Display, the image is returned by the loadBitMap() function, the core method of which is

Private Bitmap loadBitMap(String URL, int dstWidth, int dstHeight) {// Private Bitmap loadFromMemory(url); if (bitmap ! = null) { LogUtil.e(Tag, "this is from Memory"); return bitmap; } try {// bitmap = loadFromDiskCache(url, dstWidth, dstHeight); if (bitmap ! = null) { LogUtil.e(Tag, "this is from Disk"); return bitmap; } // network bitmap = loadFromNet(url, dstWidth, dstHeight); LogUtil.e(Tag, "this is from Net"); if (bitmap == null) { LogUtil.e(Tag, "bitmap null network error"); } } catch (Exception e) { e.printStackTrace(); } return bitmap; }Copy the code

You can clearly see that the logic of the displayImages() method is returned, using the same caching idea. Going back to the loadBitmapTask thread execution method, there is an important piece of logic

Bitmap mergeBitmap = mergeCallBack.merge(bitmaps, mContext, imageView); If (urls.size() == bitmaps.size()) {// Add cache try {saveDru(url, mergeBitmap); } catch (IOException e) { e.printStackTrace(); }}Copy the code

The mergeCallBack method is the user’s own image merge method, passing in a list of bitmaps, returning a merge graph object, and finally adding the merge to the cache. Next time you’ll find it directly from the cache.

The next focus is on the technique of image merging. I joined the realization of wechat and QQ group profile picture in the code, and then briefly talk about the wechat merger plan, QQ merger plan, we can go to see the code.

First let’s take a look at MergeCallBack’s implementation

@Override public Bitmap merge(List<Bitmap> bitmapArray, Context context, ImageView imageView) { this.context = context; / / width of the canvas ViewGroup. LayoutParams lp = imageView. GetLayoutParams (); int tempWidth; int tempHeight; if (lp ! = null) { tempWidth = dip2px(context, lp.width); tempHeight = dip2px(context, lp.height); } else {// Otherwise give a default height tempWidth = dip2px(context, 70); tempHeight = dip2px(context, 70); } return CombineBitmapTools.combimeBitmap(context, tempWidth, tempHeight, bitmapArray); }Copy the code

Look at the implementation of combimeBitmap

public static Bitmap combimeBitmap(Context context, int combineWidth, int combineHeight, List<Bitmap> bitmaps) { if (bitmaps == null || bitmaps.size() == 0) return null; if (bitmaps.size() >= 9) { bitmaps = bitmaps.subList(0, 9); } Bitmap resultBitmap = null; int len = bitmaps.size(); // Draw data, where all the draw coordinates are recorded. List<CombineBitmapEntity> combineBitmapEntities = CombineNineRect .generateCombineBitmapEntity(combineWidth, combineHeight, len); // thumbnailBitmaps List<Bitmap> thumbnailBitmaps = new ArrayList<Bitmap>(); for (int i = 0; i < len; i++) { thumbnailBitmaps.add(ThumbnailUtils.extractThumbnail(bitmaps.get(i), (int) combineBitmapEntities.get(i).width, (int) combineBitmapEntities.get(i).height)); } resultBitmap = getCombineBitmapentities (combineBitmapEntities, thumbnailBitmaps, combineWidth, combineHeight); return resultBitmap; } private static Bitmap getCombineBitmaps( List<CombineBitmapEntity> mEntityList, List<Bitmap> bitmaps, int width, int height) { Bitmap newBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); for (int i = 0; i < mEntityList.size(); I ++) {// newBitmap = mixtureBitmap(newBitmap, bitmaps.get(I), new PointF(mentitylist.get (I).x, mEntityList.get(i).y)); } return newBitmap; }Copy the code

Finally, call getCombineBitmaps to synthesize the image. The key to synthesize the image is through bitmap.createBitmap.

private static Bitmap mixtureBitmap(Bitmap first, Bitmap second, PointF fromPoint) { if (first == null || second == null || fromPoint == null) { return null; } Bitmap newBitmap = Bitmap.createBitmap(first.getWidth(), first.getHeight(), Bitmap.Config.ARGB_8888); Canvas cv = new Canvas(newBitmap); cv.drawBitmap(first, 0, 0, null); cv.drawBitmap(second, fromPoint.x, fromPoint.y, null); cv.save(Canvas.ALL_SAVE_FLAG); cv.restore(); if (first ! = null) { first.recycle(); first = null; } if (second ! = null) { second.recycle(); second = null; } return newBitmap; }Copy the code

All the key logic has been noted in the code.

If you want to see the full effect and complete code, you can click on my Git address MutiImgLoader. If you find it helpful, remember star