/* * Artcodes recognises a different marker scheme that allows the * creation of aesthetically pleasing, even beautiful, codes. * Copyright (C) 2013-2016 The University of Nottingham * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package uk.ac.horizon.artcodes.ui; import android.animation.Animator; import android.app.Activity; import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Handler; import android.support.v4.content.ContextCompat; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.animation.LinearInterpolator; import android.widget.ImageView; import android.widget.RelativeLayout; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import uk.ac.horizon.artcodes.R; import uk.ac.horizon.artcodes.model.Action; import uk.ac.horizon.artcodes.model.MarkerImage; /** * This class controls a relative layout to create a display of the current markers that make up * the current detected action and any future action. */ public class MarkerHistoryViewController { private static final int SEPARATOR_WIDTH_DP = 25; private static final int VIEW_WIDTH_DP = 50; private static final int IMAGE_WIDTH_DP = 45; private static final int BOTTOM_MARGIN_DP = 5; private static final int ANIMATION_DURATION_MS = 300; private final Context context; private final RelativeLayout relativeLayout; private Handler uiHandler; private List<MarkerImage> existingMarkerImages = new ArrayList<>(); private Action existingAction = null; private final float displayDensity; public MarkerHistoryViewController(Context context, RelativeLayout relativeLayout, Handler uiHandler) { this.context = context; this.relativeLayout = relativeLayout; this.uiHandler = uiHandler; DisplayMetrics displayMetrics = new DisplayMetrics(); ((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); displayDensity = displayMetrics.density; } private Map<MarkerImage, View> displayedViews = new HashMap<>(); private Map<Integer, View> missingViewsByPosition = new HashMap<>(); private Map<Integer, View> seperatorViewsByPosition = new HashMap<>(); public void update(List<MarkerImage> incomingMarkerImages, final Action currentOrFutureAction) { if (currentOrFutureAction == null || incomingMarkerImages == null || incomingMarkerImages.isEmpty()) { this.existingMarkerImages.clear(); this.existingAction = null; incomingMarkerImages = new ArrayList<>(); } final List<MarkerImage> markerImages = incomingMarkerImages; final Action.Match existingMatchType = existingAction!=null ? existingAction.getMatch() : Action.Match.any; final Action.Match currentMatchType = currentOrFutureAction!=null ? currentOrFutureAction.getMatch() : Action.Match.any; final int n = markerImages.size(); final int viewSizePX = (int) (VIEW_WIDTH_DP * displayDensity); final int separatorWidthPX= (int) (SEPARATOR_WIDTH_DP *displayDensity); final int startX = (int) (relativeLayout.getWidth() / 2 - (n * viewSizePX / 2f) - ((n-1) * separatorWidthPX / 2f)); final int finalTranslationY = relativeLayout.getHeight() - viewSizePX - (int) (BOTTOM_MARGIN_DP * displayDensity); final int finalTranslationYForSeparator = finalTranslationY + (int)(12.5*displayDensity); this.uiHandler.post(new Runnable() { @Override public void run() { // animate removal of views not in markerImages (and unneeded separators/placeholders) // TODO: instead of creating/destroying separators/placeholders they should be reused. // Remove placeholders List<Map.Entry<Integer, View>> missingToRemove = new ArrayList<>(); for (Map.Entry<Integer, View> entry : missingViewsByPosition.entrySet()) { if (entry.getKey() >= markerImages.size()-1) { missingToRemove.add(entry); } } for (Map.Entry<Integer, View> entry : missingToRemove) { final View view = entry.getValue(); animateRemoval(view); missingViewsByPosition.remove(entry.getKey()); } // Remove separators List<Map.Entry<Integer, View>> separatorsToRemove = new ArrayList<>(); if (currentOrFutureAction!=null && existingMatchType==currentMatchType) { for (Map.Entry<Integer, View> entry : seperatorViewsByPosition.entrySet()) { if (entry.getKey() >= markerImages.size() - 2) { separatorsToRemove.add(entry); } } } else { separatorsToRemove.addAll(seperatorViewsByPosition.entrySet()); } for (Map.Entry<Integer, View> entry : separatorsToRemove) { final View view = entry.getValue(); animateRemoval(view); seperatorViewsByPosition.remove(entry.getKey()); } // Remove marker images List<Map.Entry<MarkerImage, View>> toRemove = new ArrayList<>(); for (Map.Entry<MarkerImage, View> entry : displayedViews.entrySet()) { if (!markerImages.contains(entry.getKey())) { toRemove.add(entry); } } for (Map.Entry<MarkerImage, View> entry : toRemove) { final View view = entry.getValue(); animateRemoval(view); displayedViews.remove(entry.getKey()); } // Add or re-position views of images in markerImages (and separators/placeholders) int count = 0; for (MarkerImage markerImage : markerImages) { if (markerImage != null) { // add/re-position marker image final View missingView = missingViewsByPosition.get(count); if (missingView != null) { animateRemoval(missingView); missingViewsByPosition.remove(count); } View markerImageView = displayedViews.get(markerImage); if (markerImageView == null) { markerImageView = LayoutInflater.from(context).inflate(R.layout.marker_thumbnail, null); final ImageView imageView = (ImageView) markerImageView.findViewById(R.id.marker_thumbnail_image); if (imageView == null) { continue; } imageView.setImageBitmap(markerImage.image); } // Note: Translation ignores scale, and scale scales around the centre of the view. int finalTranslationX = startX + count * (viewSizePX + separatorWidthPX); if (markerImage.newDetection) { final int initialImageWidthPX = (int) (relativeLayout.getWidth() * markerImage.width); final int initialImageHeightPX = (int) (relativeLayout.getHeight() * markerImage.height); final float initialScale = (Math.max(initialImageWidthPX, initialImageHeightPX) / displayDensity) / IMAGE_WIDTH_DP; final int initialImageXCenter = (int) (relativeLayout.getWidth() * markerImage.x + initialImageWidthPX / 2); final int initialImageYCenter = (int) (relativeLayout.getHeight() * markerImage.y + initialImageHeightPX / 2); final int initialViewX = initialImageXCenter - viewSizePX / 2; final int initialViewY = initialImageYCenter - viewSizePX / 2; markerImageView.setTranslationX(initialViewX); markerImageView.setTranslationY(initialViewY); markerImageView.setScaleX(initialScale); markerImageView.setScaleY(initialScale); markerImageView.setAlpha(0f); if (!displayedViews.containsKey(markerImage)) { relativeLayout.addView(markerImageView); displayedViews.put(markerImage, markerImageView); } animateEnterOrMove(markerImageView, finalTranslationX, finalTranslationY, 1); } else { if (!displayedViews.containsKey(markerImage)) { markerImageView.setScaleX(1); markerImageView.setScaleY(1); markerImageView.setTranslationX(finalTranslationX); markerImageView.setTranslationY(finalTranslationY); markerImageView.setAlpha(0f); relativeLayout.addView(markerImageView); displayedViews.put(markerImage, markerImageView); } animateEnterOrMove(markerImageView, finalTranslationX, finalTranslationY, 1); } } else { // add/move placeholder for missing marker View view = missingViewsByPosition.get(count); final int finalTranslationX = startX + count * (viewSizePX + (int) (SEPARATOR_WIDTH_DP * displayDensity)); if (view == null) { view = createMarkerPlaceholder(); view.setTranslationX(finalTranslationX); view.setTranslationY(finalTranslationY); view.setAlpha(0f); missingViewsByPosition.put(count, view); relativeLayout.addView(view); } animateEnterOrMove(view, finalTranslationX, finalTranslationY); } // add/move separator if (count < markerImages.size()-1) { View view = seperatorViewsByPosition.get(count); final int finalTranslationX = startX + (count+1) * (viewSizePX + (int) (SEPARATOR_WIDTH_DP * displayDensity)) - (int)(SEPARATOR_WIDTH_DP *displayDensity); if (view == null) { if (currentMatchType == Action.Match.all) { view = createGroupSeparator(); } else { view = createSequenceSeparator(); } view.setTranslationX(finalTranslationX); view.setTranslationY(finalTranslationYForSeparator); view.setAlpha(0f); seperatorViewsByPosition.put(count, view); relativeLayout.addView(view); } animateEnterOrMove(view, finalTranslationX, finalTranslationYForSeparator); } ++count; } } }); this.existingMarkerImages = markerImages; this.existingAction = currentOrFutureAction; } private void animateEnterOrMove(View view, int x, int y) { view.animate() .alpha(1) .translationX(x) .translationY(y) .setDuration(ANIMATION_DURATION_MS) .setInterpolator(new LinearInterpolator()) .start(); } private void animateEnterOrMove(View view, int x, int y, float scale) { view.animate() .alpha(1) .translationX(x) .translationY(y) .scaleX(scale) .scaleY(scale) .setDuration(ANIMATION_DURATION_MS) .setInterpolator( new LinearInterpolator() ).start(); } private void animateRemoval(final View view) { view.animate() .setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { } @Override public void onAnimationEnd(Animator animator) { relativeLayout.removeView(view); } @Override public void onAnimationCancel(Animator animator) { } @Override public void onAnimationRepeat(Animator animator) { } }) .alpha(0) .setInterpolator(new LinearInterpolator()) .setDuration(ANIMATION_DURATION_MS) .start(); } private View createMarkerPlaceholder() { ImageView iv = new ImageView(context); Drawable d = ContextCompat.getDrawable(context, R.drawable.missing_marker_thumbnail_background); iv.setImageDrawable(d); return iv; } private View createGroupSeparator() { ImageView iv = new ImageView(context); Drawable d = ContextCompat.getDrawable(context, R.drawable.marker_thumbnail_separator_group_image); iv.setImageDrawable(d); return iv; } private View createSequenceSeparator() { ImageView iv = new ImageView(context); Drawable d = ContextCompat.getDrawable(context, R.drawable.marker_thumbnail_separator_sequence_image); iv.setImageDrawable(d); return iv; } }