Implementation of Multi-Graph Merging Framework

Keywords: network git

Nowadays, chatting is a very common phenomenon in most app s, and Wechat and qq are the ancestors of communication. If the product manager is considering chat design, most of them will refer to it.

Often you will hear, you see Weixin and qq are doing this, so you can come, although there are 10,000 psychological unhappy, but who calls us a pursuit of programmers?

So the requirement of the product is to realize the group portrait similar to Wechat. Similar to the following


Multi graph merging

As a programmer, I will evaluate the workload first. In the eyes of products, is it difficult to synthesize pictures together? So working hours determine what you can do.

Scheme analysis:

Solution 1. Write the layout directly, then load different pictures according to different layout. And the general image loading scheme is asynchronous loading, so that when loading, it will flash and merge a picture. The second time will be much better because there are caches in the current picture frames.

Advantages: Fast implementation

Disadvantage: Very low, not a forced programmer approach, and the effect is not good.

Solution 2. Customize a control or download all pictures asynchronously. Add a counter in the control to ensure that all pictures are displayed synchronously after downloading.

Advantages: Moderate difficulty

Disadvantage: poor scalability, which day products want to change a synthetic solution?

Scheme 3: Use native controls to merge the group images and generate a new image, then cache the original image. The merging algorithm is abstracted into an interface.

Advantages: Easy to expand, better experience

Disadvantage: Spend more time

Of course, as a programmer with a dream and a strong personality, we should consider implementing Plan 3 and benefiting some fellow programmers who are suffering from the product.

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


Merge Graph Loading Logic

First, we know that the input parameter of the program should be an ImageView control, a list of urls. There is, of course, a merge callback function to customize the merge method.

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

In line with this idea, we need to generate a new key based on urls to cache the merged image, which can be loaded directly from the cache next time. After all, merging avatars is time-consuming

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

        return sb.toString();
    }

Here's just a simple splicing of all URLs and then md5.

Cache processing is the most critical step, which involves the caching of individual linked images and the caching of merged graphs. For a caching system, single and multiple graphs are treated equally, and each key corresponds to a cached object. It's just that the rules for key are slightly different.

And the caching scheme is also a two-level cache implemented by universal Disk LruCache and Momsory LruCache, which can keep the cache efficient. (For the Lru algorithm, it's a simple Least Current Used, that is, the principle of recent use. It's not clear what Baidu is about.)

Let's look at the core code of displayImages, which is to find the memory cache first, then the disk cache, if not, then find all the single pictures synchronously.

    public void displayImages(final List<String> urls, final ImageView imageView, final MergeCallBack mergeCallBack, final int dstWidth, final int dstHeight) {
        if (urls == null || urls.size() <= 0) {
            throw new IllegalArgumentException("url Can not be empty");
        }

        if (mergeCallBack == null) {
            throw new IllegalArgumentException("mergeCallBack Can not be empty");
        }
        final String url = getNewUrlByList(urls, mergeCallBack.getMark());

        imageView.setTag(IMG_URL, url);
        //In-memory Loading
        Bitmap bitmap = loadFromMemory(url);
        if (bitmap != null) {
            LogUtil.e(Tag, "displayImages this is from Memory");
            imageView.setImageBitmap(bitmap);
            return;
        }

        try {
            //Loading on disk
            bitmap = 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 map
        bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.ic_launcher_round);
        imageView.setImageBitmap(bitmap);
        LogUtil.e(Tag, "displayImages this is from default");
        //Open a new thread and load all pictures synchronously. If the load is successful, it returns.
        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);
    }

If the load from the cache fails, we will open a thread to perform the avatar merge operation. The avatar merge is a synchronous operation. We need to get the object that needs to merge the avatar. So how do we get it? Let's continue to look at the code.

    private ArrayList<Bitmap> loadBitMaps(List<String> urls, int dstWidth, int dstHeight) {
        ArrayList<Bitmap> bitmaps = new ArrayList<>();
        for (String url : urls) {
            //Synchronized acquisition of all images
            Bitmap bitmap = loadBitMap(url, dstWidth, dstHeight);
            if (bitmap != null) {
                bitmaps.add(bitmap);
            }
        }
        return bitmaps;
    }

Display that the image is returned through the loadBitMap() function, and the core method of this function is

    private Bitmap loadBitMap(String url, int dstWidth, int dstHeight) {
        //Memory
        Bitmap bitmap = loadFromMemory(url);
        if (bitmap != null) {
            LogUtil.e(Tag, "this is from Memory");
            return bitmap;
        }

        try {
            //disk
            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;
    }

As you can see clearly, it returns to the logic of the displayImages() method and applies the same caching idea. Let's go back to load BitmapTask, the thread's execution method. One important piece of logic is

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

This merge CallBack method is an image merging method that users need to implement themselves, passing in a list of bitmap s, then returning a merge graph object, and finally we add the merge into the cache. Next time you can find it directly from the cache.

The next focus is on image merging technology. In the code, I add the group avatar that realizes Weixin and Qq. Next, I will simply talk about the scheme of Weixin merging and the scheme of QQ merging. You can see the code for yourself.

First, let's look at the implementation of MergeCallBack

@Override
public Bitmap merge(List<Bitmap> bitmapArray, Context context, ImageView imageView) {
    this.context = context;

    // Breadth of 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);
}

Look again 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 drawing coordinates are recorded.
        List<CombineBitmapEntity> combineBitmapEntities = CombineNineRect
                .generateCombineBitmapEntity(combineWidth, combineHeight, len);
        // thumbnail
        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));
        }
        // Synthesis
        resultBitmap = getCombineBitmaps(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++) {
            //Merge image
            newBitmap = mixtureBitmap(newBitmap, bitmaps.get(i), new PointF(
                    mEntityList.get(i).x, mEntityList.get(i).y));
        }
        return newBitmap;
    }

Finally, getCombine Bitmaps is called to synthesize images. The key to synthesize images 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;
    }

All the key logic has been commented in the code.

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

Posted by JohnnyBlaze on Thu, 23 May 2019 15:29:16 -0700