blob: 5e0aed566d0dfbf55b1d3ce4eac56fbd82cfc604 [file] [log] [blame]
Andy Huangf70fc402012-02-17 15:37:42 -08001/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
Andy Huang5ff63742012-03-16 20:30:23 -070018package com.android.mail.browse;
Andy Huangf70fc402012-02-17 15:37:42 -080019
20import android.content.Context;
Andy Huang46dfba62012-04-19 01:47:32 -070021import android.database.DataSetObserver;
Andy Huang59e0b182012-08-14 14:32:23 -070022import android.graphics.Canvas;
Andy Huangf70fc402012-02-17 15:37:42 -080023import android.util.AttributeSet;
Andy Huang65fe28f2012-04-06 18:08:53 -070024import android.util.SparseArray;
Andy Huangbb56a152012-02-24 18:26:47 -080025import android.view.MotionEvent;
Andy Huangf70fc402012-02-17 15:37:42 -080026import android.view.View;
Andy Huangbb56a152012-02-24 18:26:47 -080027import android.view.ViewConfiguration;
Andy Huangf70fc402012-02-17 15:37:42 -080028import android.view.ViewGroup;
29import android.webkit.WebView;
30import android.widget.Adapter;
Andy Huangb5078b22012-03-05 19:52:29 -080031import android.widget.ListView;
Andy Huangbb56a152012-02-24 18:26:47 -080032import android.widget.ScrollView;
Andy Huangf70fc402012-02-17 15:37:42 -080033
Andy Huangb5078b22012-03-05 19:52:29 -080034import com.android.mail.R;
Andy Huang5ff63742012-03-16 20:30:23 -070035import com.android.mail.browse.ScrollNotifier.ScrollListener;
Andy Huang632721e2012-04-11 16:57:26 -070036import com.android.mail.ui.ConversationViewFragment;
Andy Huang7bdc3752012-03-25 17:18:19 -070037import com.android.mail.utils.DequeMap;
Andy Huang31c38a82012-08-15 21:39:43 -070038import com.android.mail.utils.InputSmoother;
Andy Huangf70fc402012-02-17 15:37:42 -080039import com.android.mail.utils.LogUtils;
Andy Huang47aa9c92012-07-31 15:37:21 -070040import com.google.common.collect.Lists;
41
42import java.util.List;
Andy Huangb5078b22012-03-05 19:52:29 -080043
Andy Huangf70fc402012-02-17 15:37:42 -080044/**
Andy Huangb5078b22012-03-05 19:52:29 -080045 * A specialized ViewGroup container for conversation view. It is designed to contain a single
46 * {@link WebView} and a number of overlay views that draw on top of the WebView. In the Mail app,
47 * the WebView contains all HTML message bodies in a conversation, and the overlay views are the
48 * subject view, message headers, and attachment views. The WebView does all scroll handling, and
49 * this container manages scrolling of the overlay views so that they move in tandem.
50 *
51 * <h5>INPUT HANDLING</h5>
52 * Placing the WebView in the same container as the overlay views means we don't have to do a lot of
53 * manual manipulation of touch events. We do have a
54 * {@link #forwardFakeMotionEvent(MotionEvent, int)} method that deals with one WebView
55 * idiosyncrasy: it doesn't react well when touch MOVE events stream in without a preceding DOWN.
56 *
57 * <h5>VIEW RECYCLING</h5>
58 * Normally, it would make sense to put all overlay views into a {@link ListView}. But this view
59 * sandwich has unique characteristics: the list items are scrolled based on an external controller,
60 * and we happen to know all of the overlay positions up front. So it didn't make sense to shoehorn
61 * a ListView in and instead, we rolled our own view recycler by borrowing key details from
62 * ListView and AbsListView.
Andy Huangf70fc402012-02-17 15:37:42 -080063 *
64 */
65public class ConversationContainer extends ViewGroup implements ScrollListener {
66
Andy Huang632721e2012-04-11 16:57:26 -070067 private static final String TAG = ConversationViewFragment.LAYOUT_TAG;
Andy Huangf70fc402012-02-17 15:37:42 -080068
Andy Huang47aa9c92012-07-31 15:37:21 -070069 private static final int[] BOTTOM_LAYER_VIEW_IDS = {
70 R.id.webview
71 };
72
73 private static final int[] TOP_LAYER_VIEW_IDS = {
74 R.id.conversation_topmost_overlay
75 };
Andy Huang47aa9c92012-07-31 15:37:21 -070076
Andy Huang31c38a82012-08-15 21:39:43 -070077 /**
78 * Maximum scroll speed (in dp/sec) at which the snap header animation will draw.
79 * Anything faster than that, and drawing it creates visual artifacting (wagon-wheel effect).
80 */
81 private static final float SNAP_HEADER_MAX_SCROLL_SPEED = 600f;
82
Andy Huang7bdc3752012-03-25 17:18:19 -070083 private ConversationViewAdapter mOverlayAdapter;
Andy Huang2ffaeab2012-02-27 19:43:46 -080084 private int[] mOverlayBottoms;
Andy Huangbb56a152012-02-24 18:26:47 -080085 private ConversationWebView mWebView;
Andy Huang59e0b182012-08-14 14:32:23 -070086 private MessageHeaderView mSnapHeader;
87 private View mTopMostOverlay;
88
89 /**
90 * This is a hack.
91 *
92 * <p>Without this hack enabled, very fast scrolling can sometimes cause the top-most layers
93 * to skip being drawn for a frame or two. It happens specifically when overlay views are
94 * attached or added, and WebView happens to draw (on its own) immediately afterwards.
95 *
96 * <p>The workaround is to force an additional draw of the top-most overlay. Since the problem
97 * only occurs when scrolling overlays are added, restrict the additional draw to only occur
98 * if scrolling overlays were added since the last draw.
99 */
100 private boolean mAttachedOverlaySinceLastDraw;
Andy Huangbb56a152012-02-24 18:26:47 -0800101
Andy Huang47aa9c92012-07-31 15:37:21 -0700102 private final List<View> mNonScrollingChildren = Lists.newArrayList();
103
Andy Huangbb56a152012-02-24 18:26:47 -0800104 /**
Andy Huang23014702012-07-09 12:50:36 -0700105 * Current document zoom scale per {@link WebView#getScale()}. This is the ratio of actual
106 * screen pixels to logical WebView HTML pixels. We use it to convert from one to the other.
Andy Huangbb56a152012-02-24 18:26:47 -0800107 */
Andy Huangf70fc402012-02-17 15:37:42 -0800108 private float mScale;
Andy Huang120ea662012-03-27 23:15:12 -0700109 /**
110 * Set to true upon receiving the first touch event. Used to help reject invalid WebView scale
111 * values.
112 */
113 private boolean mTouchInitialized;
Andy Huangf70fc402012-02-17 15:37:42 -0800114
Andy Huangbb56a152012-02-24 18:26:47 -0800115 /**
116 * System touch-slop distance per {@link ViewConfiguration#getScaledTouchSlop()}.
117 */
118 private final int mTouchSlop;
119 /**
120 * Current scroll position, as dictated by the background {@link WebView}.
121 */
122 private int mOffsetY;
123 /**
124 * Original pointer Y for slop calculation.
125 */
126 private float mLastMotionY;
127 /**
128 * Original pointer ID for slop calculation.
129 */
130 private int mActivePointerId;
131 /**
132 * Track pointer up/down state to know whether to send a make-up DOWN event to WebView.
133 * WebView internal logic requires that a stream of {@link MotionEvent#ACTION_MOVE} events be
134 * preceded by a {@link MotionEvent#ACTION_DOWN} event.
135 */
136 private boolean mTouchIsDown = false;
137 /**
138 * Remember if touch interception was triggered on a {@link MotionEvent#ACTION_POINTER_DOWN},
139 * so we can send a make-up event in {@link #onTouchEvent(MotionEvent)}.
140 */
141 private boolean mMissedPointerDown;
142
Andy Huang7bdc3752012-03-25 17:18:19 -0700143 /**
144 * A recycler for scrap views, organized by integer item view type.
145 */
146 private final DequeMap<Integer, View> mScrapViews = new DequeMap<Integer, View>();
Andy Huangb5078b22012-03-05 19:52:29 -0800147
148 /**
Andy Huang65fe28f2012-04-06 18:08:53 -0700149 * The current set of overlay views in the view hierarchy. Looking through this map is faster
150 * than traversing the view hierarchy.
Andy Huangb5078b22012-03-05 19:52:29 -0800151 * <p>
152 * WebView sometimes notifies of scroll changes during a draw (or display list generation), when
153 * it's not safe to detach view children because ViewGroup is in the middle of iterating over
Andy Huang65fe28f2012-04-06 18:08:53 -0700154 * its child array. So we remove any child from this list immediately and queue up a task to
155 * detach it later. Since nobody other than the detach task references that view in the
156 * meantime, we don't need any further checks or synchronization.
Andy Huang46dfba62012-04-19 01:47:32 -0700157 * <p>
158 * We keep {@link OverlayView} wrappers instead of bare views so that when it's time to dispose
159 * of all views (on data set or adapter change), we can at least recycle them into the typed
160 * scrap piles for later reuse.
Andy Huangb5078b22012-03-05 19:52:29 -0800161 */
Andy Huang46dfba62012-04-19 01:47:32 -0700162 private final SparseArray<OverlayView> mOverlayViews;
Andy Huangb5078b22012-03-05 19:52:29 -0800163
Andy Huangb5078b22012-03-05 19:52:29 -0800164 private int mWidthMeasureSpec;
165
Andy Huangc7543572012-04-03 15:34:29 -0700166 private boolean mDisableLayoutTracing;
167
Andy Huang31c38a82012-08-15 21:39:43 -0700168 private final InputSmoother mVelocityTracker;
169
Andy Huang46dfba62012-04-19 01:47:32 -0700170 private final DataSetObserver mAdapterObserver = new AdapterObserver();
171
Andy Huangcf5aeae2012-03-09 17:25:08 -0800172 /**
Andy Huang59e0b182012-08-14 14:32:23 -0700173 * The adapter index of the lowest overlay item that is above the top of the screen and reports
174 * {@link ConversationOverlayItem#canPushSnapHeader()}. We calculate this after a pass through
175 * {@link #positionOverlays(int, int)}.
176 *
177 */
178 private int mSnapIndex;
179
180 /**
Andy Huangcf5aeae2012-03-09 17:25:08 -0800181 * Child views of this container should implement this interface to be notified when they are
182 * being detached.
183 *
184 */
185 public interface DetachListener {
186 /**
187 * Called on a child view when it is removed from its parent as part of
188 * {@link ConversationContainer} view recycling.
189 */
190 void onDetachedFromParent();
191 }
192
Andy Huang46dfba62012-04-19 01:47:32 -0700193 private static class OverlayView {
194 public View view;
195 int itemType;
196
197 public OverlayView(View view, int itemType) {
198 this.view = view;
199 this.itemType = itemType;
200 }
201 }
202
Andy Huangf70fc402012-02-17 15:37:42 -0800203 public ConversationContainer(Context c) {
204 this(c, null);
205 }
206
207 public ConversationContainer(Context c, AttributeSet attrs) {
208 super(c, attrs);
Andy Huangbb56a152012-02-24 18:26:47 -0800209
Andy Huang46dfba62012-04-19 01:47:32 -0700210 mOverlayViews = new SparseArray<OverlayView>();
Andy Huangb5078b22012-03-05 19:52:29 -0800211
Andy Huang31c38a82012-08-15 21:39:43 -0700212 mVelocityTracker = new InputSmoother(c);
213
Andy Huangbb56a152012-02-24 18:26:47 -0800214 mTouchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
215
216 // Disabling event splitting fixes pinch-zoom when the first pointer goes down on the
217 // WebView and the second pointer goes down on an overlay view.
218 // Intercepting ACTION_POINTER_DOWN events allows pinch-zoom to work when the first pointer
219 // goes down on an overlay view.
220 setMotionEventSplittingEnabled(false);
221 }
222
223 @Override
224 protected void onFinishInflate() {
225 super.onFinishInflate();
226
Andy Huang5ff63742012-03-16 20:30:23 -0700227 mWebView = (ConversationWebView) findViewById(R.id.webview);
Andy Huangbb56a152012-02-24 18:26:47 -0800228 mWebView.addScrollListener(this);
Andy Huang47aa9c92012-07-31 15:37:21 -0700229
Andy Huang59e0b182012-08-14 14:32:23 -0700230 mTopMostOverlay = findViewById(R.id.conversation_topmost_overlay);
231
232 mSnapHeader = (MessageHeaderView) findViewById(R.id.snap_header);
233 mSnapHeader.setSnappy(true);
234
Andy Huang47aa9c92012-07-31 15:37:21 -0700235 for (int id : BOTTOM_LAYER_VIEW_IDS) {
236 mNonScrollingChildren.add(findViewById(id));
237 }
238 for (int id : TOP_LAYER_VIEW_IDS) {
239 mNonScrollingChildren.add(findViewById(id));
240 }
Andy Huangf70fc402012-02-17 15:37:42 -0800241 }
242
Andy Huang59e0b182012-08-14 14:32:23 -0700243 public MessageHeaderView getSnapHeader() {
244 return mSnapHeader;
245 }
246
Andy Huang7bdc3752012-03-25 17:18:19 -0700247 public void setOverlayAdapter(ConversationViewAdapter a) {
Andy Huang46dfba62012-04-19 01:47:32 -0700248 if (mOverlayAdapter != null) {
249 mOverlayAdapter.unregisterDataSetObserver(mAdapterObserver);
250 clearOverlays();
251 }
Andy Huangf70fc402012-02-17 15:37:42 -0800252 mOverlayAdapter = a;
Andy Huang46dfba62012-04-19 01:47:32 -0700253 if (mOverlayAdapter != null) {
254 mOverlayAdapter.registerDataSetObserver(mAdapterObserver);
255 }
Andy Huang51067132012-03-12 20:08:19 -0700256 }
257
258 public Adapter getOverlayAdapter() {
259 return mOverlayAdapter;
Andy Huangf70fc402012-02-17 15:37:42 -0800260 }
261
Andy Huang46dfba62012-04-19 01:47:32 -0700262 private void clearOverlays() {
263 for (int i = 0, len = mOverlayViews.size(); i < len; i++) {
264 detachOverlay(mOverlayViews.valueAt(i));
265 }
266 mOverlayViews.clear();
267 }
268
269 private void onDataSetChanged() {
270 clearOverlays();
271 }
272
Andy Huangbb56a152012-02-24 18:26:47 -0800273 private void forwardFakeMotionEvent(MotionEvent original, int newAction) {
274 MotionEvent newEvent = MotionEvent.obtain(original);
275 newEvent.setAction(newAction);
276 mWebView.onTouchEvent(newEvent);
277 LogUtils.v(TAG, "in Container.OnTouch. fake: action=%d x/y=%f/%f pointers=%d",
278 newEvent.getActionMasked(), newEvent.getX(), newEvent.getY(),
279 newEvent.getPointerCount());
280 }
281
282 /**
283 * Touch slop code was copied from {@link ScrollView#onInterceptTouchEvent(MotionEvent)}.
284 */
285 @Override
286 public boolean onInterceptTouchEvent(MotionEvent ev) {
Andy Huang120ea662012-03-27 23:15:12 -0700287
288 if (!mTouchInitialized) {
289 mTouchInitialized = true;
290 }
291
Andy Huang632721e2012-04-11 16:57:26 -0700292 // no interception when WebView handles the first DOWN
293 if (mWebView.isHandlingTouch()) {
294 return false;
295 }
296
Andy Huangbb56a152012-02-24 18:26:47 -0800297 boolean intercept = false;
298 switch (ev.getActionMasked()) {
299 case MotionEvent.ACTION_POINTER_DOWN:
Andy Huang632721e2012-04-11 16:57:26 -0700300 LogUtils.d(TAG, "Container is intercepting non-primary touch!");
Andy Huangbb56a152012-02-24 18:26:47 -0800301 intercept = true;
302 mMissedPointerDown = true;
Andy Huang632721e2012-04-11 16:57:26 -0700303 requestDisallowInterceptTouchEvent(true);
Andy Huangbb56a152012-02-24 18:26:47 -0800304 break;
305
306 case MotionEvent.ACTION_DOWN:
307 mLastMotionY = ev.getY();
308 mActivePointerId = ev.getPointerId(0);
309 break;
310
311 case MotionEvent.ACTION_MOVE:
312 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
313 final float y = ev.getY(pointerIndex);
314 final int yDiff = (int) Math.abs(y - mLastMotionY);
315 if (yDiff > mTouchSlop) {
316 mLastMotionY = y;
317 intercept = true;
318 }
319 break;
320 }
321
Andy Huang632721e2012-04-11 16:57:26 -0700322// LogUtils.v(TAG, "in Container.InterceptTouch. action=%d x/y=%f/%f pointers=%d result=%s",
323// ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount(), intercept);
Andy Huangbb56a152012-02-24 18:26:47 -0800324 return intercept;
325 }
326
327 @Override
328 public boolean onTouchEvent(MotionEvent ev) {
329 final int action = ev.getActionMasked();
330
Andy Huang632721e2012-04-11 16:57:26 -0700331 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
Andy Huangbb56a152012-02-24 18:26:47 -0800332 mTouchIsDown = false;
333 } else if (!mTouchIsDown &&
334 (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_POINTER_DOWN)) {
335
336 forwardFakeMotionEvent(ev, MotionEvent.ACTION_DOWN);
337 if (mMissedPointerDown) {
338 forwardFakeMotionEvent(ev, MotionEvent.ACTION_POINTER_DOWN);
339 mMissedPointerDown = false;
340 }
341
342 mTouchIsDown = true;
343 }
344
345 final boolean webViewResult = mWebView.onTouchEvent(ev);
346
Andy Huang632721e2012-04-11 16:57:26 -0700347// LogUtils.v(TAG, "in Container.OnTouch. action=%d x/y=%f/%f pointers=%d",
348// ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount());
Andy Huangbb56a152012-02-24 18:26:47 -0800349 return webViewResult;
Andy Huangf70fc402012-02-17 15:37:42 -0800350 }
351
352 @Override
Andy Huangb5078b22012-03-05 19:52:29 -0800353 public void onNotifierScroll(final int x, final int y) {
Andy Huang31c38a82012-08-15 21:39:43 -0700354 mVelocityTracker.onInput(y);
Andy Huangc7543572012-04-03 15:34:29 -0700355 mDisableLayoutTracing = true;
Andy Huang7bdc3752012-03-25 17:18:19 -0700356 positionOverlays(x, y);
Andy Huangc7543572012-04-03 15:34:29 -0700357 mDisableLayoutTracing = false;
Andy Huangb5078b22012-03-05 19:52:29 -0800358 }
359
Andy Huang7bdc3752012-03-25 17:18:19 -0700360 private void positionOverlays(int x, int y) {
Andy Huangf70fc402012-02-17 15:37:42 -0800361 mOffsetY = y;
Andy Huang07f87322012-03-27 18:03:53 -0700362
363 /*
Andy Huang120ea662012-03-27 23:15:12 -0700364 * The scale value that WebView reports is inaccurate when measured during WebView
365 * initialization. This bug is present in ICS, so to work around it, we ignore all
Andy Huang23014702012-07-09 12:50:36 -0700366 * reported values and use a calculated expected value from ConversationWebView instead.
367 * Only when the user actually begins to touch the view (to, say, begin a zoom) do we begin
368 * to pay attention to WebView-reported scale values.
Andy Huang07f87322012-03-27 18:03:53 -0700369 */
Andy Huang120ea662012-03-27 23:15:12 -0700370 if (mTouchInitialized) {
Andy Huang07f87322012-03-27 18:03:53 -0700371 mScale = mWebView.getScale();
Andy Huang23014702012-07-09 12:50:36 -0700372 } else if (mScale == 0) {
373 mScale = mWebView.getInitialScale();
Andy Huang07f87322012-03-27 18:03:53 -0700374 }
Andy Huangc7543572012-04-03 15:34:29 -0700375 traceLayout("in positionOverlays, raw scale=%f, effective scale=%f", mWebView.getScale(),
376 mScale);
Andy Huangf70fc402012-02-17 15:37:42 -0800377
Andy Huange20e1632012-06-15 15:59:07 -0700378 if (mOverlayBottoms == null || mOverlayAdapter == null) {
Andy Huang51067132012-03-12 20:08:19 -0700379 return;
380 }
381
Andy Huangb5078b22012-03-05 19:52:29 -0800382 // recycle scrolled-off views and add newly visible views
Andy Huangb5078b22012-03-05 19:52:29 -0800383
Andy Huang7bdc3752012-03-25 17:18:19 -0700384 // we want consecutive spacers/overlays to stack towards the bottom
385 // so iterate from the bottom of the conversation up
386 // starting with the last spacer bottom and the last adapter item, position adapter views
387 // in a single stack until you encounter a non-contiguous expanded message header,
388 // then decrement to the next spacer.
389
Andy Huangc7543572012-04-03 15:34:29 -0700390 traceLayout("IN positionOverlays, spacerCount=%d overlayCount=%d", mOverlayBottoms.length,
391 mOverlayAdapter.getCount());
392
Andy Huang59e0b182012-08-14 14:32:23 -0700393 mSnapIndex = -1;
394
Andy Huang7bdc3752012-03-25 17:18:19 -0700395 int adapterIndex = mOverlayAdapter.getCount() - 1;
Andy Huangc7543572012-04-03 15:34:29 -0700396 int spacerIndex = mOverlayBottoms.length - 1;
397 while (spacerIndex >= 0 && adapterIndex >= 0) {
398
Andy Huang7bdc3752012-03-25 17:18:19 -0700399 final int spacerBottomY = getOverlayBottom(spacerIndex);
400
401 // always place at least one overlay per spacer
Andy Huang46dfba62012-04-19 01:47:32 -0700402 ConversationOverlayItem adapterItem = mOverlayAdapter.getItem(adapterIndex);
Andy Huang7bdc3752012-03-25 17:18:19 -0700403
404 int overlayBottomY = spacerBottomY;
405 int overlayTopY = overlayBottomY - adapterItem.getHeight();
406
Andy Huangc7543572012-04-03 15:34:29 -0700407 traceLayout("in loop, spacer=%d overlay=%d t/b=%d/%d (%s)", spacerIndex, adapterIndex,
408 overlayTopY, overlayBottomY, adapterItem);
Andy Huang7bdc3752012-03-25 17:18:19 -0700409 positionOverlay(adapterIndex, overlayTopY, overlayBottomY);
410
411 // and keep stacking overlays as long as they are contiguous
412 while (--adapterIndex >= 0) {
413 adapterItem = mOverlayAdapter.getItem(adapterIndex);
414 if (!adapterItem.isContiguous()) {
415 // advance to the next spacer, but stay on this adapter item
416 break;
Andy Huangb5078b22012-03-05 19:52:29 -0800417 }
Andy Huangb5078b22012-03-05 19:52:29 -0800418
Andy Huang7bdc3752012-03-25 17:18:19 -0700419 overlayBottomY = overlayTopY; // stack on top of previous overlay
420 overlayTopY = overlayBottomY - adapterItem.getHeight();
Andy Huangb5078b22012-03-05 19:52:29 -0800421
Andy Huangc7543572012-04-03 15:34:29 -0700422 traceLayout("in contig loop, spacer=%d overlay=%d t/b=%d/%d (%s)", spacerIndex,
423 adapterIndex, overlayTopY, overlayBottomY, adapterItem);
Andy Huang7bdc3752012-03-25 17:18:19 -0700424 positionOverlay(adapterIndex, overlayTopY, overlayBottomY);
Andy Huangb5078b22012-03-05 19:52:29 -0800425 }
Andy Huangc7543572012-04-03 15:34:29 -0700426
427 spacerIndex--;
Andy Huangb5078b22012-03-05 19:52:29 -0800428 }
Andy Huang59e0b182012-08-14 14:32:23 -0700429
Andy Huang31c38a82012-08-15 21:39:43 -0700430 positionSnapHeader(mSnapIndex);
Andy Huangb5078b22012-03-05 19:52:29 -0800431 }
432
433 /**
Andy Huang9875bb42012-04-04 20:36:21 -0700434 * Executes a measure pass over the specified child overlay view and returns the measured
435 * height. The measurement uses whatever the current container's width measure spec is.
436 * This method ignores view visibility and returns the height that the view would be if visible.
437 *
438 * @param overlayView an overlay view to measure. does not actually have to be attached yet.
439 * @return height that the view would be if it was visible
440 */
441 public int measureOverlay(View overlayView) {
442 measureOverlayView(overlayView);
443 return overlayView.getMeasuredHeight();
444 }
445
446 /**
Andy Huangb5078b22012-03-05 19:52:29 -0800447 * Copied/stolen from {@link ListView}.
448 */
Andy Huang9875bb42012-04-04 20:36:21 -0700449 private void measureOverlayView(View child) {
Andy Huang47aa9c92012-07-31 15:37:21 -0700450 MarginLayoutParams p = (MarginLayoutParams) child.getLayoutParams();
Andy Huangb5078b22012-03-05 19:52:29 -0800451 if (p == null) {
Andy Huang47aa9c92012-07-31 15:37:21 -0700452 p = (MarginLayoutParams) generateDefaultLayoutParams();
Andy Huangb5078b22012-03-05 19:52:29 -0800453 }
454
455 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
Andy Huang47aa9c92012-07-31 15:37:21 -0700456 getPaddingLeft() + getPaddingRight() + p.leftMargin + p.rightMargin, p.width);
Andy Huangb5078b22012-03-05 19:52:29 -0800457 int lpHeight = p.height;
458 int childHeightSpec;
459 if (lpHeight > 0) {
460 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
461 } else {
462 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
463 }
464 child.measure(childWidthSpec, childHeightSpec);
465 }
466
Andy Huang46dfba62012-04-19 01:47:32 -0700467 private void onOverlayScrolledOff(final int adapterIndex, final OverlayView overlay,
468 int overlayTop, int overlayBottom) {
Andy Huang65fe28f2012-04-06 18:08:53 -0700469 // detach the view asynchronously, as scroll notification can happen during a draw, when
470 // it's not safe to remove children
Andy Huangb5078b22012-03-05 19:52:29 -0800471
Andy Huang65fe28f2012-04-06 18:08:53 -0700472 // but immediately remove this view from the view set so future lookups don't find it
473 mOverlayViews.remove(adapterIndex);
Andy Huangb5078b22012-03-05 19:52:29 -0800474
Andy Huangb5078b22012-03-05 19:52:29 -0800475 post(new Runnable() {
476 @Override
477 public void run() {
Andy Huang46dfba62012-04-19 01:47:32 -0700478 detachOverlay(overlay);
Andy Huangb5078b22012-03-05 19:52:29 -0800479 }
480 });
481
482 // push it out of view immediately
483 // otherwise this scrolled-off header will continue to draw until the runnable runs
Andy Huang46dfba62012-04-19 01:47:32 -0700484 layoutOverlay(overlay.view, overlayTop, overlayBottom);
Andy Huangb5078b22012-03-05 19:52:29 -0800485 }
486
Andy Huang7bdc3752012-03-25 17:18:19 -0700487 public View getScrapView(int type) {
488 return mScrapViews.poll(type);
489 }
490
491 public void addScrapView(int type, View v) {
492 mScrapViews.add(type, v);
493 }
494
Andy Huang46dfba62012-04-19 01:47:32 -0700495 private void detachOverlay(OverlayView overlay) {
496 detachViewFromParent(overlay.view);
497 mScrapViews.add(overlay.itemType, overlay.view);
498 if (overlay.view instanceof DetachListener) {
499 ((DetachListener) overlay.view).onDetachedFromParent();
Andy Huangcf5aeae2012-03-09 17:25:08 -0800500 }
501 }
502
503 @Override
504 protected void onDetachedFromWindow() {
505 super.onDetachedFromWindow();
506
Andy Huang7bdc3752012-03-25 17:18:19 -0700507 mScrapViews.visitAll(new DequeMap.Visitor<View>() {
508 @Override
509 public void visit(View item) {
510 removeDetachedView(item, false /* animate */);
511 }
512 });
Andy Huangcf5aeae2012-03-09 17:25:08 -0800513 mScrapViews.clear();
Andy Huangf70fc402012-02-17 15:37:42 -0800514 }
515
516 @Override
517 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Andy Huangf70fc402012-02-17 15:37:42 -0800518 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Andy Huang632721e2012-04-11 16:57:26 -0700519 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
520 LogUtils.d(TAG, "*** IN header container onMeasure spec for w/h=%s/%s",
521 MeasureSpec.toString(widthMeasureSpec),
522 MeasureSpec.toString(heightMeasureSpec));
523 }
Andy Huangf70fc402012-02-17 15:37:42 -0800524
Andy Huang47aa9c92012-07-31 15:37:21 -0700525 for (View nonScrollingChild : mNonScrollingChildren) {
526 if (nonScrollingChild.getVisibility() != GONE) {
527 measureChildWithMargins(nonScrollingChild, widthMeasureSpec, 0 /* widthUsed */,
528 heightMeasureSpec, 0 /* heightUsed */);
529 }
Andy Huang3233bff2012-03-20 19:38:45 -0700530 }
Andy Huangb5078b22012-03-05 19:52:29 -0800531 mWidthMeasureSpec = widthMeasureSpec;
Andy Huang7bdc3752012-03-25 17:18:19 -0700532
Andy Huang9875bb42012-04-04 20:36:21 -0700533 // onLayout will re-measure and re-position overlays for the new container size, but the
534 // spacer offsets would still need to be updated to have them draw at their new locations.
Andy Huangf70fc402012-02-17 15:37:42 -0800535 }
536
537 @Override
538 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Andy Huang632721e2012-04-11 16:57:26 -0700539 LogUtils.d(TAG, "*** IN header container onLayout");
Andy Huangf70fc402012-02-17 15:37:42 -0800540
Andy Huang47aa9c92012-07-31 15:37:21 -0700541 for (View nonScrollingChild : mNonScrollingChildren) {
542 if (nonScrollingChild.getVisibility() != GONE) {
543 final int w = nonScrollingChild.getMeasuredWidth();
544 final int h = nonScrollingChild.getMeasuredHeight();
545
546 final MarginLayoutParams lp =
547 (MarginLayoutParams) nonScrollingChild.getLayoutParams();
548
549 final int childLeft = lp.leftMargin;
550 final int childTop = lp.topMargin;
551 nonScrollingChild.layout(childLeft, childTop, childLeft + w, childTop + h);
552 }
553 }
Andy Huangf70fc402012-02-17 15:37:42 -0800554
Andy Huange20e1632012-06-15 15:59:07 -0700555 if (mOverlayAdapter != null) {
556 // being in a layout pass means overlay children may require measurement,
557 // so invalidate them
558 for (int i = 0, len = mOverlayAdapter.getCount(); i < len; i++) {
559 mOverlayAdapter.getItem(i).invalidateMeasurement();
560 }
Andy Huang8778a872012-04-20 17:50:41 -0700561 }
562
563 positionOverlays(0, mOffsetY);
Andy Huang9875bb42012-04-04 20:36:21 -0700564 }
565
Andy Huang47aa9c92012-07-31 15:37:21 -0700566 @Override
Andy Huang59e0b182012-08-14 14:32:23 -0700567 protected void dispatchDraw(Canvas canvas) {
568 super.dispatchDraw(canvas);
569
570 if (mAttachedOverlaySinceLastDraw) {
571 drawChild(canvas, mTopMostOverlay, getDrawingTime());
572 mAttachedOverlaySinceLastDraw = false;
573 }
574 }
575
576 @Override
Andy Huang47aa9c92012-07-31 15:37:21 -0700577 protected LayoutParams generateDefaultLayoutParams() {
578 return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
579 }
580
581 @Override
582 public LayoutParams generateLayoutParams(AttributeSet attrs) {
583 return new MarginLayoutParams(getContext(), attrs);
584 }
585
586 @Override
587 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
588 return new MarginLayoutParams(p);
589 }
590
591 @Override
592 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
593 return p instanceof MarginLayoutParams;
594 }
595
Andy Huang7bdc3752012-03-25 17:18:19 -0700596 private int getOverlayBottom(int spacerIndex) {
Andy Huangb5078b22012-03-05 19:52:29 -0800597 // TODO: round or truncate?
Andy Huang7bdc3752012-03-25 17:18:19 -0700598 return (int) (mOverlayBottoms[spacerIndex] * mScale);
599 }
600
601 private void positionOverlay(int adapterIndex, int overlayTopY, int overlayBottomY) {
Andy Huang46dfba62012-04-19 01:47:32 -0700602 final OverlayView overlay = mOverlayViews.get(adapterIndex);
603 final ConversationOverlayItem item = mOverlayAdapter.getItem(adapterIndex);
Andy Huang9875bb42012-04-04 20:36:21 -0700604
Andy Huang31c38a82012-08-15 21:39:43 -0700605 // save off the item's current top for later snap calculations
606 item.setTop(overlayTopY);
607
Andy Huangc7543572012-04-03 15:34:29 -0700608 // is the overlay visible and does it have non-zero height?
609 if (overlayTopY != overlayBottomY && overlayBottomY > mOffsetY
610 && overlayTopY < mOffsetY + getHeight()) {
Andy Huang46dfba62012-04-19 01:47:32 -0700611 View overlayView = overlay != null ? overlay.view : null;
Andy Huang7bdc3752012-03-25 17:18:19 -0700612 // show and/or move overlay
613 if (overlayView == null) {
614 overlayView = addOverlayView(adapterIndex);
Andy Huang9875bb42012-04-04 20:36:21 -0700615 measureOverlayView(overlayView);
616 item.markMeasurementValid();
617 traceLayout("show/measure overlay %d", adapterIndex);
Andy Huangc7543572012-04-03 15:34:29 -0700618 } else {
619 traceLayout("move overlay %d", adapterIndex);
Andy Huang9875bb42012-04-04 20:36:21 -0700620 if (!item.isMeasurementValid()) {
621 measureOverlayView(overlayView);
622 item.markMeasurementValid();
623 traceLayout("and (re)measure overlay %d, old/new heights=%d/%d", adapterIndex,
624 overlayView.getHeight(), overlayView.getMeasuredHeight());
625 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700626 }
Andy Huang9875bb42012-04-04 20:36:21 -0700627 traceLayout("laying out overlay %d with h=%d", adapterIndex,
628 overlayView.getMeasuredHeight());
Andy Huang65fe28f2012-04-06 18:08:53 -0700629 layoutOverlay(overlayView, overlayTopY, overlayTopY + overlayView.getMeasuredHeight());
Andy Huang7bdc3752012-03-25 17:18:19 -0700630 } else {
631 // hide overlay
Andy Huang46dfba62012-04-19 01:47:32 -0700632 if (overlay != null) {
Andy Huangc7543572012-04-03 15:34:29 -0700633 traceLayout("hide overlay %d", adapterIndex);
Andy Huang46dfba62012-04-19 01:47:32 -0700634 onOverlayScrolledOff(adapterIndex, overlay, overlayTopY, overlayBottomY);
Andy Huangc7543572012-04-03 15:34:29 -0700635 } else {
636 traceLayout("ignore non-visible overlay %d", adapterIndex);
Andy Huang7bdc3752012-03-25 17:18:19 -0700637 }
638 }
Andy Huang59e0b182012-08-14 14:32:23 -0700639
640 if (overlayTopY <= mOffsetY && item.canPushSnapHeader()) {
641 if (mSnapIndex == -1) {
642 mSnapIndex = adapterIndex;
643 } else if (adapterIndex > mSnapIndex) {
644 mSnapIndex = adapterIndex;
645 }
646 }
647
Andy Huang7bdc3752012-03-25 17:18:19 -0700648 }
649
650 // layout an existing view
651 // need its top offset into the conversation, its height, and the scroll offset
Andy Huangc7543572012-04-03 15:34:29 -0700652 private void layoutOverlay(View child, int childTop, int childBottom) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700653 final int top = childTop - mOffsetY;
Andy Huangc7543572012-04-03 15:34:29 -0700654 final int bottom = childBottom - mOffsetY;
Andy Huang47aa9c92012-07-31 15:37:21 -0700655
656 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
657 final int childLeft = getPaddingLeft() + lp.leftMargin;
658
659 child.layout(childLeft, top, childLeft + child.getMeasuredWidth(), bottom);
Andy Huangb5078b22012-03-05 19:52:29 -0800660 }
661
Andy Huang7bdc3752012-03-25 17:18:19 -0700662 private View addOverlayView(int adapterIndex) {
663 final int itemType = mOverlayAdapter.getItemViewType(adapterIndex);
664 final View convertView = mScrapViews.poll(itemType);
Andy Huangf70fc402012-02-17 15:37:42 -0800665
Andy Huang7bdc3752012-03-25 17:18:19 -0700666 View view = mOverlayAdapter.getView(adapterIndex, convertView, this);
Andy Huang46dfba62012-04-19 01:47:32 -0700667 mOverlayViews.put(adapterIndex, new OverlayView(view, itemType));
Andy Huang7bdc3752012-03-25 17:18:19 -0700668
Andy Huang59e0b182012-08-14 14:32:23 -0700669 final int index = BOTTOM_LAYER_VIEW_IDS.length;
Andy Huang47aa9c92012-07-31 15:37:21 -0700670
Andy Huang7bdc3752012-03-25 17:18:19 -0700671 // Only re-attach if the view had previously been added to a view hierarchy.
672 // Since external components can contribute to the scrap heap (addScrapView), we can't
673 // assume scrap views had already been attached.
674 if (view.getRootView() != view) {
Andy Huangc7543572012-04-03 15:34:29 -0700675 LogUtils.d(TAG, "want to REUSE scrolled-in view: index=%d obj=%s", adapterIndex, view);
Andy Huang47aa9c92012-07-31 15:37:21 -0700676 attachViewToParent(view, index, view.getLayoutParams());
Andy Huang7bdc3752012-03-25 17:18:19 -0700677 } else {
Andy Huangc7543572012-04-03 15:34:29 -0700678 LogUtils.d(TAG, "want to CREATE scrolled-in view: index=%d obj=%s", adapterIndex, view);
Andy Huang47aa9c92012-07-31 15:37:21 -0700679 addViewInLayout(view, index, view.getLayoutParams(),
Andy Huang7bdc3752012-03-25 17:18:19 -0700680 true /* preventRequestLayout */);
Andy Huangf70fc402012-02-17 15:37:42 -0800681 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700682
Andy Huang59e0b182012-08-14 14:32:23 -0700683 mAttachedOverlaySinceLastDraw = true;
684
Andy Huang7bdc3752012-03-25 17:18:19 -0700685 return view;
Andy Huangf70fc402012-02-17 15:37:42 -0800686 }
687
Andy Huang31c38a82012-08-15 21:39:43 -0700688 // render and/or re-position snap header
689 private void positionSnapHeader(int snapIndex) {
690 ConversationOverlayItem snapItem = null;
691 if (snapIndex != -1) {
692 final ConversationOverlayItem item = mOverlayAdapter.getItem(snapIndex);
693 if (item.canBecomeSnapHeader()) {
694 snapItem = item;
695 }
696 }
697 if (snapItem == null) {
698 mSnapHeader.setVisibility(GONE);
699 mSnapHeader.unbind();
700 return;
701 }
702
703 snapItem.bindView(mSnapHeader, false /* measureOnly */);
704 mSnapHeader.setVisibility(VISIBLE);
705
706 int overlap = 0;
707
708 final ConversationOverlayItem next = findNextPushingOverlay(snapIndex + 1);
709 if (next != null) {
710 overlap = Math.min(0, next.getTop() - mSnapHeader.getHeight() - mOffsetY);
711
712 // disable overlap drawing past a certain speed
713 if (overlap < 0) {
714 final Float v = mVelocityTracker.getSmoothedVelocity();
715 if (v != null && v > SNAP_HEADER_MAX_SCROLL_SPEED) {
716 overlap = 0;
717 }
718 }
719 }
720 mSnapHeader.setTranslateY(overlap);
721 }
722
723 // find the next header that can push the snap header up
724 private ConversationOverlayItem findNextPushingOverlay(int start) {
725 int value = -1;
726 for (int i = start, len = mOverlayAdapter.getCount(); i < len; i++) {
727 final ConversationOverlayItem next = mOverlayAdapter.getItem(i);
728 if (next.canPushSnapHeader()) {
729 return next;
730 }
731 }
732 return null;
733 }
734
Andy Huangc7543572012-04-03 15:34:29 -0700735 /**
736 * Prevents any layouts from happening until the next time {@link #onGeometryChange(int[])} is
737 * called. Useful when you know the HTML spacer coordinates are inconsistent with adapter items.
738 * <p>
739 * If you call this, you must ensure that a followup call to {@link #onGeometryChange(int[])}
740 * is made later, when the HTML spacer coordinates are updated.
741 *
742 */
743 public void invalidateSpacerGeometry() {
744 mOverlayBottoms = null;
745 }
746
Andy Huang7bdc3752012-03-25 17:18:19 -0700747 public void onGeometryChange(int[] overlayBottoms) {
Andy Huangc7543572012-04-03 15:34:29 -0700748 traceLayout("*** got overlay spacer bottoms:");
Andy Huang7bdc3752012-03-25 17:18:19 -0700749 for (int offsetY : overlayBottoms) {
Andy Huangc7543572012-04-03 15:34:29 -0700750 traceLayout("%d", offsetY);
Andy Huangf70fc402012-02-17 15:37:42 -0800751 }
752
Andy Huang7bdc3752012-03-25 17:18:19 -0700753 mOverlayBottoms = overlayBottoms;
754 positionOverlays(0, mOffsetY);
Andy Huangf70fc402012-02-17 15:37:42 -0800755 }
756
Andy Huangc7543572012-04-03 15:34:29 -0700757 private void traceLayout(String msg, Object... params) {
758 if (mDisableLayoutTracing) {
759 return;
760 }
Andy Huang7f9ef602012-07-25 16:44:30 -0700761 LogUtils.d(TAG, msg, params);
Andy Huangc7543572012-04-03 15:34:29 -0700762 }
763
Andy Huang46dfba62012-04-19 01:47:32 -0700764 private class AdapterObserver extends DataSetObserver {
765 @Override
766 public void onChanged() {
767 onDataSetChanged();
768 }
769 }
770
Andy Huangf70fc402012-02-17 15:37:42 -0800771}