package io.nextop.view;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewParent;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import io.nextop.rx.RxDebugger;
import javax.annotation.Nullable;
import java.util.*;
import java.util.concurrent.TimeUnit;
// TODO visualization and interactions to support the debug fragment
public class DebugOverlayView extends View {
public static final Object TAG = new Object();
@Nullable
private ViewSummary rootViewSummary = null;
// temp draw state
final Paint paint = new Paint();
final int[] wloc = new int[2];
long nanos;
public DebugOverlayView(Context context) {
super(context);
init();
}
public DebugOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public DebugOverlayView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@SuppressLint("NewApi")
public DebugOverlayView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init();
}
private void init() {
setTag(TAG);
}
// FIXME
// visualizations/interactions:
// - press and hold over debug fragment and drag, to resize debug fragment
// - listen to Debugger.getStats() and maintain a list of stats to display (keyed by subscriber)
// -- draw stats either 1. over the associated view 2. in the bottom right as a circle coming out, representing no view
// TODO is there ever a case of going from view to no view? seems rare (don't need an animation for this)
// TODO flash the region when there is an update to a region
public void setRootViewSummary(@Nullable ViewSummary rootViewSummary) {
this.rootViewSummary = rootViewSummary;
invalidate();
// TODO check update times, run a poller until final animation time
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (null != rootViewSummary) {
// - draw the root in the bottom right
// - draw the view summaries recursively
nanos = System.nanoTime();
for (ViewSummary vs : rootViewSummary.nearestDescendants) {
drawViewSummary(canvas, vs);
}
}
}
// FIXME saw a SO here
private void drawViewSummary(Canvas canvas, ViewSummary viewSummary) {
// FIXME
View rview = viewSummary.view;
float rw = rview.getWidth();
float rh = rview.getHeight();
rview.getLocationInWindow(wloc);
float rx = wloc[0];
float ry = wloc[1];
getLocationInWindow(wloc);
rx -= wloc[0];
ry -= wloc[1];
float p = 2.f;
// FIXME lerpColor, maskColor utils (just port processing)
// FIXME colors etc
int millis = (int) TimeUnit.NANOSECONDS.toMillis(nanos - viewSummary.nanos);
int fc, sc;
float sw;
if (millis < 1000) {
fc = Color.argb(35 * (1000 - millis) / 1000, 255, 0, 0);
sc = Color.argb(255, 255, 0, 0);
sw = 2.f + 3 * (1000 - millis) / 1000.f;
} else {
fc = 0;
sc = Color.argb(255, 255, 0, 0);
sw = 2.f;
}
if (0 < Color.alpha(fc)) {
paint.setStyle(Paint.Style.FILL);
paint.setColor(fc);
canvas.drawRect(rx + p, ry + p, rx + rw - 2 * p, ry + rh - 2 * p, paint);
}
if (0 < Color.alpha(sc)) {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(sw);
paint.setColor(sc);
canvas.drawRect(rx + p, ry + p, rx + rw - 2 * p, ry + rh - 2 * p, paint);
}
// FIXME
// summary text
if (millis < 2000) {
// count + type
paint.setStyle(Paint.Style.FILL);
// paint.setStrokeWidth(1.f);
paint.setTextAlign(Paint.Align.LEFT);
paint.setTextSize(32.f);
paint.setColor(Color.argb(220, 255, 0, 0));
canvas.drawText(String.format("%d", viewSummary.netOnNextCount), rx + 16, ry + rh - 16, paint);
}
for (ViewSummary vs : viewSummary.nearestDescendants) {
drawViewSummary(canvas, vs);
}
}
public static class ViewSummary {
// take the flags from the most recent update
public final int flags;
public final long nanos;
@Nullable
public final View view;
public final int netOnNextCount;
public final int netOnCompletedCount;
public final int netOnErrorCount;
public final int netFailedNotificationCount;
/** this reflects the descendants in the view hierarchy
* that have no closer common descendant/ancestor */
public final List<ViewSummary> nearestDescendants;
ViewSummary(final int flags, @Nullable View view,
int netOnNextCount, int netOnCompletedCount, int netOnErrorCount,
int netFailedNotificationCount,
final long nanos,
List<ViewSummary> nearestDescendants) {
this.nanos = nanos;
this.view = view;
this.netOnNextCount = netOnNextCount;
this.netOnCompletedCount = netOnCompletedCount;
this.netOnErrorCount = netOnErrorCount;
this.netFailedNotificationCount = netFailedNotificationCount;
this.flags = flags;
this.nearestDescendants = nearestDescendants;
}
public static ViewSummary create(Collection<RxDebugger.Stats> allStats) {
Multimap<View, RxDebugger.Stats> statsMap = ArrayListMultimap.create();
for (RxDebugger.Stats stats : allStats) {
@Nullable View v;
if (null == stats.view
|| View.VISIBLE != stats.view.getWindowVisibility()
|| View.VISIBLE != stats.view.getVisibility()) {
v = null;
} else {
v = stats.view;
}
statsMap.put(v, stats);
}
// create the graph
Map<Object, View> nearestAncestors = new HashMap<Object, View>(statsMap.size());
Multimap<View, View> nearestDescendants = ArrayListMultimap.create();
Set<View> views = statsMap.keySet();
for (@Nullable View view : views) {
if (null != view) {
@Nullable View nearestAncestor;
if (!nearestAncestors.containsKey(view)) {
ViewParent p;
View a = null;
for (p = view.getParent(); null != p; p = p.getParent()) {
if (nearestAncestors.containsKey(p)) {
a = nearestAncestors.get(p);
break;
} else if (views.contains(p)) {
a = (View) p;
break;
}
}
nearestAncestor = a;
// propagate down
nearestAncestors.put(view, nearestAncestor);
for (ViewParent q = view.getParent(); q != p; q = q.getParent()) {
nearestAncestors.put(q, nearestAncestor);
}
} else {
nearestAncestor = nearestAncestors.get(view);
}
nearestDescendants.put(nearestAncestor, view);
}
}
return create(statsMap, nearestDescendants, null);
}
private static ViewSummary create(Multimap<View, RxDebugger.Stats> statsMap,
Multimap<View, View> nearestDescendants, @Nullable View view) {
// roll up the stats
long mostRecentNanos = -1L;
int mostRecentFlags = 0;
int netOnNextCount = 0;
int netOnCompletedCount = 0;
int netOnErrorCount = 0;
int netFailedNotificationCount = 0;
for (RxDebugger.Stats stats : statsMap.get(view)) {
if (mostRecentNanos < stats.nanos) {
mostRecentNanos = stats.nanos;
mostRecentFlags = stats.flags;
}
netOnNextCount += stats.onNextCount;
netOnCompletedCount += stats.onCompletedCount;
netOnErrorCount += stats.onErrorCount;
netFailedNotificationCount += stats.failedNotificationCount;
}
Collection<View> ndVs = nearestDescendants.get(view);
List<ViewSummary> ndVss = new ArrayList<ViewSummary>(ndVs.size());
for (View nearestDescendant : ndVs) {
ndVss.add(create(statsMap, nearestDescendants, nearestDescendant));
}
return new ViewSummary(mostRecentFlags, view,
netOnNextCount, netOnCompletedCount, netOnErrorCount,
netFailedNotificationCount,
mostRecentNanos,
ndVss);
}
}
}