<NME> ImageGalleryView.java
<BEF> package cgeo.geocaching.ui;

import cgeo.geocaching.ImageViewActivity;
import cgeo.geocaching.R;
import cgeo.geocaching.apps.navi.NavigationAppFactory;
import cgeo.geocaching.databinding.ImageGalleryCategoryBinding;
import cgeo.geocaching.databinding.ImageGalleryImageBinding;
import cgeo.geocaching.databinding.ImageGalleryViewBinding;
import cgeo.geocaching.enumerations.LoadFlags;
import cgeo.geocaching.enumerations.WaypointType;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.location.Geopoint;
import cgeo.geocaching.models.Geocache;
import cgeo.geocaching.models.Image;
import cgeo.geocaching.models.Waypoint;
import cgeo.geocaching.service.GeocacheChangedBroadcastReceiver;
import cgeo.geocaching.storage.DataStore;
import cgeo.geocaching.ui.dialog.ContextMenuDialog;
import cgeo.geocaching.ui.dialog.SimpleDialog;
import cgeo.geocaching.ui.recyclerview.AbstractRecyclerViewHolder;
import cgeo.geocaching.ui.recyclerview.ManagedListAdapter;
import cgeo.geocaching.utils.CategorizedListHelper;
import cgeo.geocaching.utils.CollectionStream;
import cgeo.geocaching.utils.ImageDataMemoryCache;
import cgeo.geocaching.utils.ImageUtils;
import cgeo.geocaching.utils.LocalizationUtils;
import cgeo.geocaching.utils.MetadataUtils;
import cgeo.geocaching.utils.ShareUtils;
import cgeo.geocaching.utils.UriUtils;
import cgeo.geocaching.utils.functions.Action2;
import cgeo.geocaching.utils.functions.Func1;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.HtmlCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

public class ImageGalleryView extends LinearLayout {


public class ImageGalleryView extends LinearLayout {

    private Context context;
    private Activity activity;

    private ImageActivityHelper imageHelper = null;
    private final ImageDataMemoryCache imageDataMemoryCache = new ImageDataMemoryCache(2);

    private ImageListAdapter adapter;
    private RecyclerView view;

    private String geocode;

    private int imageSizeDp = 150;

    private final Map<String, Geopoint> imageCoordMap = new HashMap<>();
    private final Map<String, Integer> categoryCounts = new HashMap<>();
    private final Map<String, EditableCategoryHandler> editableCategoryHandlerMap = new HashMap<>();

    public interface EditableCategoryHandler {
        Collection<Image> getAllImages();

        Collection<Image> add(Collection<Image> images);

        void delete(Image image);

        Image setTitle(Image image, String title);

    }
    @Override
    protected void onConfigurationChanged(final Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        resetListLayout(view, newConfig);
    }

    private void resetListLayout(final RecyclerView view, final Configuration newConfig) {

        this.imageSizeDp = 160;
        int colCount = newConfig.screenWidthDp / (this.imageSizeDp + 10);

        //provide at least 2 columns
        if (colCount == 1 && newConfig.screenWidthDp > 100) {
            colCount = 2;
            this.imageSizeDp = (newConfig.screenWidthDp / 2) / 10;
        }
        final int colCountFinal = colCount;

        final GridLayoutManager lm = new GridLayoutManager(view.getContext(), colCount);
        lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(final int position) {
                return adapter.getItem(position).isCategoryHeader ? colCountFinal : 1;
            }
        });
        lm.getSpanSizeLookup().setSpanIndexCacheEnabled(true);
        lm.getSpanSizeLookup().setSpanGroupIndexCacheEnabled(true);
        view.setLayoutManager(lm);
    }

    private void resetListLayoutCache() {
        ((GridLayoutManager) this.view.getLayoutManager()).getSpanSizeLookup().invalidateSpanIndexCache();
        ((GridLayoutManager) this.view.getLayoutManager()).getSpanSizeLookup().invalidateSpanGroupIndexCache();
    }

    private static void setImageLayoutSizes(final ImageGalleryImageBinding image, final int imageSizeInDp) {
        final int imageSizeInner = imageSizeInDp - 10;
        setLayoutSize(image.imageComplete, imageSizeInDp, -1);
        setLayoutSize(image.imageWrapper, imageSizeInner, imageSizeInner);
        setLayoutSize(image.imageImage, imageSizeInner, imageSizeInner);
        setLayoutSize(image.imageTitle, imageSizeInner, -1);
    }

    private static void setLayoutSize(final View view, final int widthInDp, final int heightInDp) {

    }



    @Override
    protected void onConfigurationChanged(final Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        recalculateLayout(newConfig);
    }

    private void recalculateLayout(final Configuration newConfig) {

        final GridLayoutManager manager = (GridLayoutManager) binding.imagegalleryList.getLayoutManager();
        final int colCount = Math.max(1, newConfig.screenWidthDp / (IMAGE_TARGET_SIZE_DB + 10));
        manager.setSpanCount(colCount);
        manager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(final int pos) {
                return adapter.getItem(pos).isHeader() ? colCount : 1;
            }
        });
        manager.getSpanSizeLookup().setSpanGroupIndexCacheEnabled(true);
        manager.getSpanSizeLookup().setSpanIndexCacheEnabled(true);
    }

    private void invalidateSpanIndexCaches() {
        final GridLayoutManager manager = (GridLayoutManager) binding.imagegalleryList.getLayoutManager();
        manager.getSpanSizeLookup().invalidateSpanGroupIndexCache();
        manager.getSpanSizeLookup().invalidateSpanIndexCache();
    }

    private static class ImageGalleryEntry {

        public final Image image;
        public final String category;

        ImageGalleryEntry(final String category) {
            this(category, null);
        }

        ImageGalleryEntry(final String category, final Image image) {
            this.image = image;
            this.category = category;
        }

        public boolean isHeader() {
            return image == null;
        }
    }

    public void fillView(final ImageGalleryEntryHolder viewHolder, final ImageGalleryImageBinding binding) {

        final ImageGalleryEntry imgData = viewHolder.entry;

        public final Image image;
        public final String category;
        public final boolean isCategoryHeader;

        ImageListData(final String category) {
            this(category, null);
        }

        ImageListData(final String category, final Image image) {
            this.image = image;
            this.category = category;
            this.isCategoryHeader = image == null;
        }

    }

    private static class ImageViewHolder extends RecyclerView.ViewHolder {

        public final ImageGalleryCategoryBinding categoryBinding;
        public final ImageGalleryImageBinding imageBinding;

        ImageViewHolder(@NonNull final View itemView, final boolean isCategoryHeader) {
            super(itemView);
            if (isCategoryHeader) {
                categoryBinding = ImageGalleryCategoryBinding.bind(itemView);
                imageBinding = null;
            } else {
                categoryBinding = null;
                imageBinding = ImageGalleryImageBinding.bind(itemView);
            }
        }
    }

                binding.imageImage.setImageDrawable(p.first);

        protected ImageListAdapter(final RecyclerView view) {
            super(new ManagedListAdapter.Config(view).setNotifyOnEvents(true));
            resetListLayout(view, getContext().getResources().getConfiguration());
        }

        @NonNull
        @Override
        public ImageViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
            final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
            return new ImageViewHolder(view, viewType == R.layout.image_gallery_category);
        }

        @Override
        public void onBindViewHolder(@NonNull final ImageViewHolder holder, final int position) {
            if (getItem(position).isCategoryHeader) {
                final String category = getItem(position).category;
                final ImageGalleryCategoryBinding binding = holder.categoryBinding;
                binding.imgGalleryCategoryTitle.setText(category);

                final boolean isEditableCat = editableCategoryHandlerMap.containsKey(category);
                binding.imgGalleryAddButtons.setVisibility(isEditableCat ? View.VISIBLE : View.GONE);
                binding.imgGalleryAddCamera.setOnClickListener(v -> imageHelper.getImageFromCamera(geocode, false, category));
                binding.imgGalleryAddMulti.setOnClickListener(v -> imageHelper.getMultipleImagesFromStorage(geocode, false, category));

            } else {
                final Image img = getItem(position).image;
                final ImageGalleryImageBinding binding = holder.imageBinding;
                setImageLayoutSizes(binding, imageSizeDp);
                if (!StringUtils.isBlank(img.getTitle())) {
                    binding.imageTitle.setText(TextParam.text(img.getTitle()).setHtml(true).getText(getContext()));
                    binding.imageTitle.setVisibility(View.VISIBLE);
                } else {
                    binding.imageTitle.setVisibility(View.GONE);
                }
                binding.imageDescriptionMarker.setVisibility(img.hasDescription() ? View.VISIBLE : View.GONE);

                imageDataMemoryCache.loadImage(img.getUrl(), p -> {
                    final BitmapDrawable bd = p.first;

                    //check if request is still valid
                    final ImageListData ild = getItem(holder.getBindingAdapterPosition());
                    if (ild == null || ild.image == null || !ild.image.getUrl().equals(img.getUrl())) {
                        return;
                    }
                    final Geopoint gp = MetadataUtils.getFirstGeopoint(p.second);

                    binding.imageImage.setImageDrawable(bd);
                    binding.imageImage.setVisibility(View.VISIBLE);
                    if (gp != null) {
                        binding.imageGeoOverlay.setVisibility(View.VISIBLE);
                        imageCoordMap.put(img.getUrl(), gp);
                    } else {
                        binding.imageGeoOverlay.setVisibility(View.GONE);
                    }
                    binding.imageProgressBar.setVisibility(View.GONE);
                }, () -> {
                    binding.imageProgressBar.setVisibility(View.VISIBLE);
                    binding.imageImage.setVisibility(View.GONE);
                    binding.imageGeoOverlay.setVisibility(View.GONE);
                });

                binding.imageImage.setOnClickListener(v -> {
                    final Image image = getItem(holder.getBindingAdapterPosition()).image;
                    viewInStandardApp(image);
                });
                binding.imageImage.setOnLongClickListener(v -> {
                    final ImageListData ild = getItem(holder.getBindingAdapterPosition());
                    showContextOptions(ild.image, holder.getBindingAdapterPosition(), ild.category);
                    return true;
                });

            }
        }

        @Override
        public int getItemViewType(final int position) {
            return getItem(position).isCategoryHeader ? R.layout.image_gallery_category : R.layout.image_gallery_image;
        }
    }


        ImageViewActivity.enableViewTransitions(this.activity);
    }

    /** (re)sets contextual information for gallery */
    public void setup(final String geocode) {
        this.geocode = geocode;
        this.imageDataMemoryCache.setCode(this.geocode);
    }

    /** gets total number of images currently displayed in this gallery */
    public int getImageCount() {
        return categorizedImageListHelper.getContentSize();
    }

    /** adds images for display to this gallery */
    public void addImages(final Collection<Image> images) {
        setOrientation(VERTICAL);
        this.context = new ContextThemeWrapper(getContext(), R.style.cgeo);
        this.activity = ViewUtils.toActivity(this.context);
        final ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(), R.style.cgeo);
        inflate(ctw, R.layout.image_gallery_view, this);
        final ImageGalleryViewBinding listViewBinding = ImageGalleryViewBinding.bind(this);
        this.view = listViewBinding.imageGalleryList;
        this.adapter = new ImageListAdapter(listViewBinding.imageGalleryList);
     }

    public void setup(final String geocode) {

    private void addImagesInternal(final Collection<Image> images, final Func1<Image, String> categoryMapper, final boolean force) {

        //order images by category
        final List<String> categories = new ArrayList<>();
        final Map<String, List<ImageGalleryEntry>> catImages = new HashMap<>();
        for (Image img : images) {
            final String category = categoryMapper.call(img);
            if (!force && editableCategoryHandlerMap.containsKey(category)) {
                continue;
            }
            if (!catImages.containsKey(category)) {
                categories.add(category);
                catImages.put(category, new ArrayList<>());
            }
            catImages.get(category).add(new ImageGalleryEntry(category, img));
        }

        //add to gallery category-wise
        for (String category : categories) {

            if (!categorizedImageListHelper.containsCategory(category)) {
                createCategory(category, false);

            if (!catImages.containsKey(category)) {
                catImages.put(category, new ArrayList<>());

                //new category, create it
                adapter.addItem(category == null ? 0 : adapter.getItemCount(), new ImageListData(category));
                this.categoryCounts.put(category, 0);
            }
            catImages.get(category).add(new ImageListData(category, img));
        }

        for (Map.Entry<String, List<ImageListData>> catImage : catImages.entrySet()) {
            final int catIdx = getCategoryIndex(catImage.getKey());
            adapter.addItems(catIdx + 1, catImage.getValue());
            categoryCounts.put(catImage.getKey(), categoryCounts.get(catImage.getKey()) + catImage.getValue().size());
        }
        resetListLayoutCache();
    }

    public void setEditableCategory(final String category, final EditableCategoryHandler handler) {

    /** adds an editable category to this gallery. This is a category where user has options to add/rename/remove images */
            return;
        }
        removeCategory(category);

        this.editableCategoryHandlerMap.put(category, handler);
        this.categoryCounts.put(category, 0);

        //fill initially
        addImagesInternal(handler.getAllImages(), i -> category, true);
        createCategory(category, true);

        //fill initially
        addImagesInternal(handler.getAllImages(), i -> category, true);

        if (imageHelper == null) {
            imageHelper = new ImageActivityHelper(activity, (r, imgUris, uk) -> {
                final Collection<Image> imgs = CollectionStream.of(imgUris).map(uri -> new Image.Builder().setUrl(uri).build()).toList();
                final Collection<Image> addedImgs = editableCategoryHandlerMap.get(uk).add(imgs);
                if (r == ImageActivityHelper.REQUEST_CODE_CAMERA) {
                    //change title of camera photos
                    final Collection<Image> copy = new ArrayList<>(addedImgs);
        return imageHelper.onActivityResult(requestCode, resultCode, data);
    }

    private int getCategoryIndex(final String category) {
        //TODO implement more efficient algo
        for (int i = 0; i < adapter.getItemCount(); i++) {
            final ImageListData v = adapter.getItem(i);
            if (Objects.equals(category, v.category)) {
                return i;
            }
        }
        return -1;
    }

    public void clear() {
        this.imageDataMemoryCache.clear();
        this.categoryCounts.clear();
        this.adapter.clearList();
    }


    private void removeCategory(final String category) {
        if (!categoryCounts.containsKey(category)) {
            return;
        }
        final int idx = getCategoryIndex(category);
        for (int i = idx + categoryCounts.get(category); i >= idx; i--) {
            adapter.removeItem(i);
        }
        categoryCounts.remove(category);
    }

    @Override
        this.imageCountChangeCallback = callback;
    }

    /** scrolls the image gallery to the image index given */
        this.imageDataMemoryCache.clear();
    }

    private static void setTextIfPossible(final String htmlText, final View parentView, final int viewId) {
        if (StringUtils.isBlank(htmlText) || parentView == null || viewId == 0) {
            return;
        }
        final View view = parentView.findViewById(viewId);
        if (view instanceof TextView) {
            ((TextView) view).setText(HtmlCompat.fromHtml(htmlText, HtmlCompat.FROM_HTML_MODE_LEGACY));
            view.setVisibility(View.VISIBLE);
        } else if (view != null) {
            view.setVisibility(View.GONE);
        }
    }


    private void viewInStandardApp(final Image img) {
        if (activity == null) {
            return;
    public View getImageViewForContentIndex(final int contentIndex) {
        final int pos = categorizedImageListHelper.getListIndexForContentIndex(contentIndex);
        if (pos == -1) {
            return null;
        }
        final ImageGalleryEntryHolder vh = (ImageGalleryEntryHolder) binding.imagegalleryList.findViewHolderForAdapterPosition(pos);
        if (vh == null || vh.imageBinding == null) {
            return null;
        }
        return vh.imageBinding.imageImage;
        ImageUtils.viewImageInStandardApp(activity, imgUri);
    }

    private void showContextOptions(final Image img, final int pos, final String category) {
        if (activity == null) {
            return;
        }
        this.imageCount = 0;
    }

    /** registers the activity associated with this gallery for image transtions towards image detail view */
    public void registerCallerActivity() {
        ImageViewActivity.registerCallerActivity(this.activity, this::getImageViewForContentIndex);
    }

    public void removeCategory(final String category) {
        if (!categorizedImageListHelper.containsCategory(category)) {
            return;
        }

        changeImageCount(-categorizedImageListHelper.getCategoryCount(category));
        final int titlePos = categorizedImageListHelper.getCategoryTitlePosition(category);
        for (int pos = categorizedImageListHelper.getCategoryInsertPosition(category) - 1; pos >= titlePos; pos--) {
            adapter.removeItem(pos);
        }
        categorizedImageListHelper.removeCategory(category);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        //clear image cache
        this.imageDataMemoryCache.clear();
    }

    /** Important: include this in your activity's "onActivityReenter" method.
     * Needed e.g. for correct back transition when returning from detail view (e.g. scrolling to image gallery position) */
    public static int onActivityReenter(final Activity activity, final ImageGalleryView imageGallery, final Intent data) {
        final EditableCategoryHandler editHandler = editableCategoryHandlerMap.get(category);
        if (editHandler != null) {
            ctxMenu.addItem(LocalizationUtils.getString(R.string.log_image_delete), R.drawable.ic_menu_delete, v -> {
                this.adapter.removeItem(pos);
                this.categoryCounts.put(category, this.categoryCounts.get(category) - 1);
                editHandler.delete(img);
            });
        }
        return pos;
    }

    /** initializes gallery to a certain position.
     * Use this method directly for gallery recreation if gallery was not available in activity's "onActivityReender" method */
    public void initializeToPosition(final int contentIndex) {
        final int realPos = Math.max(0, Math.min(adapter.getItemCount() - 1, contentIndex));
        this.allowOpenViewer();
        this.scrollTo(realPos);
        this.post(() -> activity.startPostponedEnterTransition());
    }

    /** allows the gallery to open a image detail view (again) */
    private void allowOpenViewer() {
        this.allowOpenViewer = true;
    }

    private void openImageDetails(final ImageGalleryEntry imgData, final ImageGalleryImageBinding binding) {
        if (imgData.image.isImageOrUnknownUri()) {
            openImageViewer(binding);
        } else {
            ShareUtils.openContentForView(getContext(), imgData.image.getUrl());
        }
    }

    private void openImageViewer(final ImageGalleryImageBinding binding) {
        if (activity == null || !allowOpenViewer) {
            return;
        }
        allowOpenViewer = false;

        final int intentPos = this.binding.imagegalleryList.getChildAdapterPosition(binding.getRoot());
        final int contentPos = categorizedImageListHelper.getContentIndexForListIndex(intentPos);

        final List<Image> images = new ArrayList<>();
        for (ImageGalleryEntry item : adapter.getItems()) {
            if (!item.isHeader()) {
                images.add(item.image);
            }
        }

        ImageViewActivity.openImageView(this.activity, geocode, images, contentPos,
                this::getImageViewForContentIndex);
    }

    // splitting up that method would not help improve readability
    @SuppressWarnings({"PMD.NPathComplexity", "PMD.ExcessiveMethodLength"})
    private void showContextOptions(final ImageGalleryEntry imgData, final ImageGalleryImageBinding binding) {
        if (activity == null || imgData == null || imgData.image == null) {
            return;
        }
        final String category = imgData.category;
        final Image img = imgData.image;

        final int intentPos = this.binding.imagegalleryList.getChildAdapterPosition(binding.getRoot());

        final ContextMenuDialog ctxMenu = new ContextMenuDialog(activity);
        ctxMenu.setTitle(LocalizationUtils.getString(R.string.cache_image));

        ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_image_open), R.drawable.ic_menu_image,
                v -> openImageDetails(imgData, binding));

        ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_image_open_file), R.drawable.ic_menu_image,
                v -> ImageUtils.viewImageInStandardApp(activity, img.getUri(), geocode));

        if (!UriUtils.isFileUri(img.getUri()) && !UriUtils.isContentUri(img.getUri())) {
            ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_image_open_browser), R.drawable.ic_menu_open_in_browser,
                    v -> ShareUtils.openUrl(context, img.getUrl(), false));
        }
        if (img.hasDescription()) {
            ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_image_show_description), R.drawable.ic_menu_hint,
                    v -> SimpleDialog.ofContext(getContext()).setTitle(R.string.log_image_description)
                            .setMessage(TextParam.text(img.getDescription()).setHtml(true)).show());
        }
        final Geopoint gp = imageCoordMap.get(img.getUrl());
        if (gp != null && geocode != null) {
            ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_image_add_waypoint), R.drawable.ic_menu_myposition, v -> {
                final Geocache geocache = DataStore.loadCache(geocode, LoadFlags.LOAD_CACHE_OR_DB);
                if (geocache != null) {
                    final Waypoint waypoint = new Waypoint(img.getTitle(), WaypointType.WAYPOINT, true);
                    waypoint.setCoords(gp);
                    geocache.addOrChangeWaypoint(waypoint, true);
                    GeocacheChangedBroadcastReceiver.sendBroadcast(activity, geocode);
                }
            });
            ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_menu_navigate), R.drawable.ic_menu_navigate,
                    v -> NavigationAppFactory.showNavigationMenu(activity, null, null, gp));
        }

        final EditableCategoryHandler editHandler = editableCategoryHandlerMap.get(category);
        if (editHandler != null) {

            final Image oldImg = adapter.getItem(intentPos).image;
            ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_image_rename), R.drawable.ic_menu_edit, v -> SimpleDialog.ofContext(getContext()).setTitle(TextParam.id(R.string.cache_image_rename))
                .input(-1, oldImg.getTitle(), null, null, newTitle -> {
                    if (!StringUtils.isBlank(newTitle)) {
                        final Image newImg = editHandler.setTitle(oldImg, newTitle);
                        if (newImg != null) {
                            final ImageGalleryEntry oldEntry = adapter.getItem(intentPos);
                            adapter.updateItem(new ImageGalleryEntry(oldEntry.category, newImg), intentPos);
                        }
                    }
                }));

            ctxMenu.addItem(LocalizationUtils.getString(R.string.cache_image_delete), R.drawable.ic_menu_delete, v -> {
                final ImageGalleryEntry oldEntry = adapter.getItem(intentPos);
                adapter.removeItem(intentPos);
                editHandler.delete(img);
                categorizedImageListHelper.addToCount(oldEntry.category, -1);
                changeImageCount(-1);
            });
        }
        ctxMenu.show();
    }

}

<MSG> rel to #12783: change layout to allow margins to left and right of categories (#12875)


<DFF> @@ -5,7 +5,6 @@ import cgeo.geocaching.R;
 import cgeo.geocaching.apps.navi.NavigationAppFactory;
 import cgeo.geocaching.databinding.ImageGalleryCategoryBinding;
 import cgeo.geocaching.databinding.ImageGalleryImageBinding;
-import cgeo.geocaching.databinding.ImageGalleryViewBinding;
 import cgeo.geocaching.enumerations.LoadFlags;
 import cgeo.geocaching.enumerations.WaypointType;
 import cgeo.geocaching.location.Geopoint;
@@ -41,11 +40,9 @@ import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
-import androidx.core.text.HtmlCompat;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
@@ -62,21 +59,24 @@ import org.apache.commons.lang3.StringUtils;
 
 public class ImageGalleryView extends LinearLayout {
 
+    private static final int IMAGE_TARGET_SIZE_DB = 160;
+    private static final int IMAGE_SPACE_DB = 10;
+
     private Context context;
     private Activity activity;
 
     private ImageActivityHelper imageHelper = null;
     private final ImageDataMemoryCache imageDataMemoryCache = new ImageDataMemoryCache(2);
 
-    private ImageListAdapter adapter;
-    private RecyclerView view;
-
     private String geocode;
 
     private int imageSizeDp = 150;
+    private int columnCount = 2;
+    private int categoryHorizontalMargin = 2;
 
     private final Map<String, Geopoint> imageCoordMap = new HashMap<>();
-    private final Map<String, Integer> categoryCounts = new HashMap<>();
+    private final Map<String, ImageGalleryCategoryBinding> categoryViews = new HashMap<>();
+    private final Map<String, ImageListAdapter> categoryAdapters = new HashMap<>();
     private final Map<String, EditableCategoryHandler> editableCategoryHandlerMap = new HashMap<>();
 
     public interface EditableCategoryHandler {
@@ -92,44 +92,49 @@ public class ImageGalleryView extends LinearLayout {
     @Override
     protected void onConfigurationChanged(final Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
-        resetListLayout(view, newConfig);
+        recalculateLayout(newConfig);
     }
 
-    private void resetListLayout(final RecyclerView view, final Configuration newConfig) {
+    private void recalculateLayout(final Configuration newConfig) {
 
-        this.imageSizeDp = 160;
-        int colCount = newConfig.screenWidthDp / (this.imageSizeDp + 10);
+        this.imageSizeDp = IMAGE_TARGET_SIZE_DB;
+        this.columnCount = Math.max(1, newConfig.screenWidthDp / (this.imageSizeDp + 10));
 
         //provide at least 2 columns
-        if (colCount == 1 && newConfig.screenWidthDp > 100) {
-            colCount = 2;
-            this.imageSizeDp = (newConfig.screenWidthDp / 2) / 10;
+        if (this.columnCount == 1 && newConfig.screenWidthDp > 100) {
+            this.columnCount = 2;
+            this.imageSizeDp = (newConfig.screenWidthDp / 2) - 10;
         }
-        final int colCountFinal = colCount;
+        this.categoryHorizontalMargin = Math.max(0,
+            (newConfig.screenWidthDp - this.columnCount * (this.imageSizeDp + IMAGE_SPACE_DB)) / 2 - 10);
 
-        final GridLayoutManager lm = new GridLayoutManager(view.getContext(), colCount);
-        lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
-            @Override
-            public int getSpanSize(final int position) {
-                return adapter.getItem(position).isCategoryHeader ? colCountFinal : 1;
-            }
-        });
-        lm.getSpanSizeLookup().setSpanIndexCacheEnabled(true);
-        lm.getSpanSizeLookup().setSpanGroupIndexCacheEnabled(true);
-        view.setLayoutManager(lm);
+        //apply new layout
+        for (ImageGalleryCategoryBinding categoryView : this.categoryViews.values()) {
+            setListLayout(categoryView);
+        }
     }
 
-    private void resetListLayoutCache() {
-        ((GridLayoutManager) this.view.getLayoutManager()).getSpanSizeLookup().invalidateSpanIndexCache();
-        ((GridLayoutManager) this.view.getLayoutManager()).getSpanSizeLookup().invalidateSpanGroupIndexCache();
+    private void setListLayout(final ImageGalleryCategoryBinding categoryView) {
+        final MarginLayoutParams mlp = (MarginLayoutParams) categoryView.imageGalleryList.getLayoutParams();
+        final int mh = ViewUtils.dpToPixel(this.categoryHorizontalMargin);
+        mlp.setMargins(mh, 0, mh, 0);
+
+        final int colCountFinal = columnCount;
+        final GridLayoutManager lm = new GridLayoutManager(categoryView.imageGalleryList.getContext(), colCountFinal);
+        categoryView.imageGalleryList.setLayoutManager(lm);
     }
 
-    private static void setImageLayoutSizes(final ImageGalleryImageBinding image, final int imageSizeInDp) {
+    private void setImageLayoutSizes(final ImageGalleryImageBinding image, final int imageSizeInDp) {
         final int imageSizeInner = imageSizeInDp - 10;
         setLayoutSize(image.imageComplete, imageSizeInDp, -1);
         setLayoutSize(image.imageWrapper, imageSizeInner, imageSizeInner);
         setLayoutSize(image.imageImage, imageSizeInner, imageSizeInner);
         setLayoutSize(image.imageTitle, imageSizeInner, -1);
+
+        final MarginLayoutParams mlp = (MarginLayoutParams) image.imageComplete.getLayoutParams();
+        final int mh = ViewUtils.dpToPixel(IMAGE_SPACE_DB) / 2;
+        mlp.setMargins(0, mh, 0, mh);
+
     }
 
     private static void setLayoutSize(final View view, final int widthInDp, final int heightInDp) {
@@ -190,34 +195,21 @@ public class ImageGalleryView extends LinearLayout {
 
         public final Image image;
         public final String category;
-        public final boolean isCategoryHeader;
-
-        ImageListData(final String category) {
-            this(category, null);
-        }
 
         ImageListData(final String category, final Image image) {
             this.image = image;
             this.category = category;
-            this.isCategoryHeader = image == null;
         }
 
     }
 
     private static class ImageViewHolder extends RecyclerView.ViewHolder {
 
-        public final ImageGalleryCategoryBinding categoryBinding;
         public final ImageGalleryImageBinding imageBinding;
 
-        ImageViewHolder(@NonNull final View itemView, final boolean isCategoryHeader) {
+        ImageViewHolder(@NonNull final View itemView) {
             super(itemView);
-            if (isCategoryHeader) {
-                categoryBinding = ImageGalleryCategoryBinding.bind(itemView);
-                imageBinding = null;
-            } else {
-                categoryBinding = null;
-                imageBinding = ImageGalleryImageBinding.bind(itemView);
-            }
+            imageBinding = ImageGalleryImageBinding.bind(itemView);
         }
     }
 
@@ -225,82 +217,65 @@ public class ImageGalleryView extends LinearLayout {
 
         protected ImageListAdapter(final RecyclerView view) {
             super(new ManagedListAdapter.Config(view).setNotifyOnEvents(true));
-            resetListLayout(view, getContext().getResources().getConfiguration());
         }
 
         @NonNull
         @Override
         public ImageViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
-            final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
-            return new ImageViewHolder(view, viewType == R.layout.image_gallery_category);
+            final View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.image_gallery_image, parent, false);
+            return new ImageViewHolder(view);
         }
 
         @Override
         public void onBindViewHolder(@NonNull final ImageViewHolder holder, final int position) {
-            if (getItem(position).isCategoryHeader) {
-                final String category = getItem(position).category;
-                final ImageGalleryCategoryBinding binding = holder.categoryBinding;
-                binding.imgGalleryCategoryTitle.setText(category);
-
-                final boolean isEditableCat = editableCategoryHandlerMap.containsKey(category);
-                binding.imgGalleryAddButtons.setVisibility(isEditableCat ? View.VISIBLE : View.GONE);
-                binding.imgGalleryAddCamera.setOnClickListener(v -> imageHelper.getImageFromCamera(geocode, false, category));
-                binding.imgGalleryAddMulti.setOnClickListener(v -> imageHelper.getMultipleImagesFromStorage(geocode, false, category));
 
+            final Image img = getItem(position).image;
+            final ImageGalleryImageBinding binding = holder.imageBinding;
+            if (!StringUtils.isBlank(img.getTitle())) {
+                binding.imageTitle.setText(TextParam.text(img.getTitle()).setHtml(true).getText(getContext()));
+                binding.imageTitle.setVisibility(View.VISIBLE);
             } else {
-                final Image img = getItem(position).image;
-                final ImageGalleryImageBinding binding = holder.imageBinding;
-                setImageLayoutSizes(binding, imageSizeDp);
-                if (!StringUtils.isBlank(img.getTitle())) {
-                    binding.imageTitle.setText(TextParam.text(img.getTitle()).setHtml(true).getText(getContext()));
-                    binding.imageTitle.setVisibility(View.VISIBLE);
-                } else {
-                    binding.imageTitle.setVisibility(View.GONE);
+                binding.imageTitle.setVisibility(View.GONE);
+            }
+            setImageLayoutSizes(binding, imageSizeDp);
+            binding.imageDescriptionMarker.setVisibility(img.hasDescription() ? View.VISIBLE : View.GONE);
+
+            imageDataMemoryCache.loadImage(img.getUrl(), p -> {
+                final BitmapDrawable bd = p.first;
+
+                //check if request is still valid
+                final ImageListData ild = getItem(holder.getBindingAdapterPosition());
+                if (ild == null || ild.image == null || !ild.image.getUrl().equals(img.getUrl())) {
+                    return;
                 }
-                binding.imageDescriptionMarker.setVisibility(img.hasDescription() ? View.VISIBLE : View.GONE);
-
-                imageDataMemoryCache.loadImage(img.getUrl(), p -> {
-                    final BitmapDrawable bd = p.first;
-
-                    //check if request is still valid
-                    final ImageListData ild = getItem(holder.getBindingAdapterPosition());
-                    if (ild == null || ild.image == null || !ild.image.getUrl().equals(img.getUrl())) {
-                        return;
-                    }
-                    final Geopoint gp = MetadataUtils.getFirstGeopoint(p.second);
-
-                    binding.imageImage.setImageDrawable(bd);
-                    binding.imageImage.setVisibility(View.VISIBLE);
-                    if (gp != null) {
-                        binding.imageGeoOverlay.setVisibility(View.VISIBLE);
-                        imageCoordMap.put(img.getUrl(), gp);
-                    } else {
-                        binding.imageGeoOverlay.setVisibility(View.GONE);
-                    }
-                    binding.imageProgressBar.setVisibility(View.GONE);
-                }, () -> {
-                    binding.imageProgressBar.setVisibility(View.VISIBLE);
-                    binding.imageImage.setVisibility(View.GONE);
+                final Geopoint gp = MetadataUtils.getFirstGeopoint(p.second);
+
+                binding.imageImage.setImageDrawable(bd);
+                binding.imageImage.setVisibility(View.VISIBLE);
+                if (gp != null) {
+                    binding.imageGeoOverlay.setVisibility(View.VISIBLE);
+                    imageCoordMap.put(img.getUrl(), gp);
+                } else {
                     binding.imageGeoOverlay.setVisibility(View.GONE);
-                });
-
-                binding.imageImage.setOnClickListener(v -> {
-                    final Image image = getItem(holder.getBindingAdapterPosition()).image;
-                    viewInStandardApp(image);
-                });
-                binding.imageImage.setOnLongClickListener(v -> {
-                    final ImageListData ild = getItem(holder.getBindingAdapterPosition());
-                    showContextOptions(ild.image, holder.getBindingAdapterPosition(), ild.category);
-                    return true;
-                });
+                }
+                binding.imageProgressBar.setVisibility(View.GONE);
+            }, () -> {
+                binding.imageProgressBar.setVisibility(View.VISIBLE);
+                binding.imageImage.setVisibility(View.GONE);
+                binding.imageGeoOverlay.setVisibility(View.GONE);
+            });
 
-            }
+            binding.imageImage.setOnClickListener(v -> {
+                final Image image = getItem(holder.getBindingAdapterPosition()).image;
+                viewInStandardApp(image);
+            });
+            binding.imageImage.setOnLongClickListener(v -> {
+                final ImageListData ild = getItem(holder.getBindingAdapterPosition());
+                showContextOptions(ild.image, holder.getBindingAdapterPosition(), ild.category, this);
+                return true;
+            });
         }
 
-        @Override
-        public int getItemViewType(final int position) {
-            return getItem(position).isCategoryHeader ? R.layout.image_gallery_category : R.layout.image_gallery_image;
-        }
     }
 
 
@@ -323,11 +298,7 @@ public class ImageGalleryView extends LinearLayout {
         setOrientation(VERTICAL);
         this.context = new ContextThemeWrapper(getContext(), R.style.cgeo);
         this.activity = ViewUtils.toActivity(this.context);
-        final ContextThemeWrapper ctw = new ContextThemeWrapper(getContext(), R.style.cgeo);
-        inflate(ctw, R.layout.image_gallery_view, this);
-        final ImageGalleryViewBinding listViewBinding = ImageGalleryViewBinding.bind(this);
-        this.view = listViewBinding.imageGalleryList;
-        this.adapter = new ImageListAdapter(listViewBinding.imageGalleryList);
+        recalculateLayout(getContext().getResources().getConfiguration());
      }
 
     public void setup(final String geocode) {
@@ -357,20 +328,43 @@ public class ImageGalleryView extends LinearLayout {
 
             if (!catImages.containsKey(category)) {
                 catImages.put(category, new ArrayList<>());
-
-                //new category, create it
-                adapter.addItem(category == null ? 0 : adapter.getItemCount(), new ImageListData(category));
-                this.categoryCounts.put(category, 0);
             }
             catImages.get(category).add(new ImageListData(category, img));
         }
 
         for (Map.Entry<String, List<ImageListData>> catImage : catImages.entrySet()) {
-            final int catIdx = getCategoryIndex(catImage.getKey());
-            adapter.addItems(catIdx + 1, catImage.getValue());
-            categoryCounts.put(catImage.getKey(), categoryCounts.get(catImage.getKey()) + catImage.getValue().size());
+            if (!categoryViews.containsKey(catImage.getKey())) {
+                createCategory(catImage.getKey());
+            }
+            final ImageListAdapter adapter = categoryAdapters.get(catImage.getKey());
+            adapter.addItems(adapter.getItemCount(), catImage.getValue());
         }
-        resetListLayoutCache();
+    }
+
+    private void createCategory(final String category) {
+        if (this.categoryViews.containsKey(category)) {
+            return;
+        }
+
+        final ImageGalleryCategoryBinding binding = ImageGalleryCategoryBinding.inflate(LayoutInflater.from(this.context), this, false);
+        if (category == null) {
+            binding.imgGalleryCategoryTitle.setVisibility(View.GONE);
+        } else {
+            binding.imgGalleryCategoryTitle.setVisibility(View.VISIBLE);
+            binding.imgGalleryCategoryTitle.setText(category);
+        }
+
+        final boolean isEditableCat = this.editableCategoryHandlerMap.containsKey(category);
+        binding.imgGalleryAddButtons.setVisibility(isEditableCat ? View.VISIBLE : View.GONE);
+        binding.imgGalleryAddCamera.setOnClickListener(v -> imageHelper.getImageFromCamera(geocode, false, category));
+        binding.imgGalleryAddMulti.setOnClickListener(v -> imageHelper.getMultipleImagesFromStorage(geocode, false, category));
+
+        this.addView(binding.getRoot(), category == null ? 0 : this.getChildCount(), new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+
+        this.categoryViews.put(category, binding);
+        final ImageListAdapter adapter = new ImageListAdapter(binding.imageGalleryList);
+        this.categoryAdapters.put(category, adapter);
+        setListLayout(binding);
     }
 
     public void setEditableCategory(final String category, final EditableCategoryHandler handler) {
@@ -379,9 +373,8 @@ public class ImageGalleryView extends LinearLayout {
             return;
         }
         removeCategory(category);
-
         this.editableCategoryHandlerMap.put(category, handler);
-        this.categoryCounts.put(category, 0);
+        createCategory(category);
 
         //fill initially
         addImagesInternal(handler.getAllImages(), i -> category, true);
@@ -400,33 +393,27 @@ public class ImageGalleryView extends LinearLayout {
         return imageHelper.onActivityResult(requestCode, resultCode, data);
     }
 
-    private int getCategoryIndex(final String category) {
-        //TODO implement more efficient algo
-        for (int i = 0; i < adapter.getItemCount(); i++) {
-            final ImageListData v = adapter.getItem(i);
-            if (Objects.equals(category, v.category)) {
-                return i;
-            }
-        }
-        return -1;
-    }
-
     public void clear() {
         this.imageDataMemoryCache.clear();
-        this.categoryCounts.clear();
-        this.adapter.clearList();
+        this.removeAllViews();
+        this.categoryAdapters.clear();
+        this.categoryViews.clear();
     }
 
-
-    private void removeCategory(final String category) {
-        if (!categoryCounts.containsKey(category)) {
+    public void removeCategory(final String category) {
+        if (!categoryViews.containsKey(category)) {
             return;
         }
-        final int idx = getCategoryIndex(category);
-        for (int i = idx + categoryCounts.get(category); i >= idx; i--) {
-            adapter.removeItem(i);
+
+        for (int i = 0; i < getChildCount(); i++) {
+            final ImageGalleryCategoryBinding view = ImageGalleryCategoryBinding.bind(getChildAt(i));
+            if (Objects.equals(category, view.imgGalleryCategoryTitle.getText().toString())) {
+                removeView(view.getRoot());
+                break;
+            }
         }
-        categoryCounts.remove(category);
+        categoryViews.remove(category);
+        categoryAdapters.remove(category);
     }
 
     @Override
@@ -437,20 +424,6 @@ public class ImageGalleryView extends LinearLayout {
         this.imageDataMemoryCache.clear();
     }
 
-    private static void setTextIfPossible(final String htmlText, final View parentView, final int viewId) {
-        if (StringUtils.isBlank(htmlText) || parentView == null || viewId == 0) {
-            return;
-        }
-        final View view = parentView.findViewById(viewId);
-        if (view instanceof TextView) {
-            ((TextView) view).setText(HtmlCompat.fromHtml(htmlText, HtmlCompat.FROM_HTML_MODE_LEGACY));
-            view.setVisibility(View.VISIBLE);
-        } else if (view != null) {
-            view.setVisibility(View.GONE);
-        }
-    }
-
-
     private void viewInStandardApp(final Image img) {
         if (activity == null) {
             return;
@@ -467,7 +440,7 @@ public class ImageGalleryView extends LinearLayout {
         ImageUtils.viewImageInStandardApp(activity, imgUri);
     }
 
-    private void showContextOptions(final Image img, final int pos, final String category) {
+    private void showContextOptions(final Image img, final int pos, final String category, final ImageListAdapter adapter) {
         if (activity == null) {
             return;
         }
@@ -506,8 +479,7 @@ public class ImageGalleryView extends LinearLayout {
         final EditableCategoryHandler editHandler = editableCategoryHandlerMap.get(category);
         if (editHandler != null) {
             ctxMenu.addItem(LocalizationUtils.getString(R.string.log_image_delete), R.drawable.ic_menu_delete, v -> {
-                this.adapter.removeItem(pos);
-                this.categoryCounts.put(category, this.categoryCounts.get(category) - 1);
+                adapter.removeItem(pos);
                 editHandler.delete(img);
             });
         }

rel to #12783: change layout to allow margins to left and right of categories (#12875)