blob: 19380eafba22b6c7797f7dbd30ffb706cb266ece [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 Huang8f187782012-11-06 17:49:25 -080021import android.content.res.Configuration;
Andy Huang46dfba62012-04-19 01:47:32 -070022import android.database.DataSetObserver;
Andrew Sappersteind97eb872014-04-16 16:48:03 -070023import android.graphics.Canvas;
Andrew Sapperstein2fd167d2014-01-28 10:07:38 -080024import android.support.v4.view.ViewCompat;
Andy Huangf70fc402012-02-17 15:37:42 -080025import android.util.AttributeSet;
Andy Huang65fe28f2012-04-06 18:08:53 -070026import android.util.SparseArray;
Andy Huangadbf3e82012-10-13 13:30:19 -070027import android.view.Gravity;
Andy Huangbb56a152012-02-24 18:26:47 -080028import android.view.MotionEvent;
Andy Huangf70fc402012-02-17 15:37:42 -080029import android.view.View;
Andy Huangbb56a152012-02-24 18:26:47 -080030import android.view.ViewConfiguration;
Andy Huangf70fc402012-02-17 15:37:42 -080031import android.view.ViewGroup;
32import android.webkit.WebView;
33import android.widget.Adapter;
Andy Huangb5078b22012-03-05 19:52:29 -080034import android.widget.ListView;
Andy Huangbb56a152012-02-24 18:26:47 -080035import android.widget.ScrollView;
Andy Huangf70fc402012-02-17 15:37:42 -080036
Andy Huangb5078b22012-03-05 19:52:29 -080037import com.android.mail.R;
Andy Huang5ff63742012-03-16 20:30:23 -070038import com.android.mail.browse.ScrollNotifier.ScrollListener;
Andy Huang8f187782012-11-06 17:49:25 -080039import com.android.mail.providers.UIProvider;
Andy Huang632721e2012-04-11 16:57:26 -070040import com.android.mail.ui.ConversationViewFragment;
Andy Huang7bdc3752012-03-25 17:18:19 -070041import com.android.mail.utils.DequeMap;
Andy Huang31c38a82012-08-15 21:39:43 -070042import com.android.mail.utils.InputSmoother;
Andy Huangf70fc402012-02-17 15:37:42 -080043import com.android.mail.utils.LogUtils;
Andy Huang47aa9c92012-07-31 15:37:21 -070044import com.google.common.collect.Lists;
45
46import java.util.List;
Andy Huangb5078b22012-03-05 19:52:29 -080047
Andy Huangf70fc402012-02-17 15:37:42 -080048/**
Andy Huangb5078b22012-03-05 19:52:29 -080049 * A specialized ViewGroup container for conversation view. It is designed to contain a single
50 * {@link WebView} and a number of overlay views that draw on top of the WebView. In the Mail app,
51 * the WebView contains all HTML message bodies in a conversation, and the overlay views are the
52 * subject view, message headers, and attachment views. The WebView does all scroll handling, and
53 * this container manages scrolling of the overlay views so that they move in tandem.
54 *
55 * <h5>INPUT HANDLING</h5>
56 * Placing the WebView in the same container as the overlay views means we don't have to do a lot of
57 * manual manipulation of touch events. We do have a
58 * {@link #forwardFakeMotionEvent(MotionEvent, int)} method that deals with one WebView
59 * idiosyncrasy: it doesn't react well when touch MOVE events stream in without a preceding DOWN.
60 *
61 * <h5>VIEW RECYCLING</h5>
62 * Normally, it would make sense to put all overlay views into a {@link ListView}. But this view
63 * sandwich has unique characteristics: the list items are scrolled based on an external controller,
64 * and we happen to know all of the overlay positions up front. So it didn't make sense to shoehorn
65 * a ListView in and instead, we rolled our own view recycler by borrowing key details from
Andrew Sappersteind97eb872014-04-16 16:48:03 -070066 * ListView and AbsListView.
Andy Huangf70fc402012-02-17 15:37:42 -080067 *
68 */
69public class ConversationContainer extends ViewGroup implements ScrollListener {
Andy Huang632721e2012-04-11 16:57:26 -070070 private static final String TAG = ConversationViewFragment.LAYOUT_TAG;
Andy Huangf70fc402012-02-17 15:37:42 -080071
Andy Huang47aa9c92012-07-31 15:37:21 -070072 private static final int[] BOTTOM_LAYER_VIEW_IDS = {
Andrew Sapperstein14f93742013-07-25 14:29:56 -070073 R.id.webview,
74 R.id.conversation_side_border_overlay
Andy Huang47aa9c92012-07-31 15:37:21 -070075 };
76
77 private static final int[] TOP_LAYER_VIEW_IDS = {
78 R.id.conversation_topmost_overlay
79 };
Andy Huang47aa9c92012-07-31 15:37:21 -070080
Andy Huang31c38a82012-08-15 21:39:43 -070081 /**
82 * Maximum scroll speed (in dp/sec) at which the snap header animation will draw.
83 * Anything faster than that, and drawing it creates visual artifacting (wagon-wheel effect).
84 */
85 private static final float SNAP_HEADER_MAX_SCROLL_SPEED = 600f;
86
Andy Huang8f187782012-11-06 17:49:25 -080087 private ConversationAccountController mAccountController;
Andy Huang7bdc3752012-03-25 17:18:19 -070088 private ConversationViewAdapter mOverlayAdapter;
Andy Huangadbf3e82012-10-13 13:30:19 -070089 private OverlayPosition[] mOverlayPositions;
Andy Huangbb56a152012-02-24 18:26:47 -080090 private ConversationWebView mWebView;
Andrew Sappersteina467d402013-10-15 18:45:03 -070091 private SnapHeader mSnapHeader;
Andrew Sappersteind97eb872014-04-16 16:48:03 -070092 private View mTopMostOverlay;
93
94 /**
95 * This is a hack.
96 *
97 * <p>Without this hack enabled, very fast scrolling can sometimes cause the top-most layers
98 * to skip being drawn for a frame or two. It happens specifically when overlay views are
99 * attached or added, and WebView happens to draw (on its own) immediately afterwards.
100 *
101 * <p>The workaround is to force an additional draw of the top-most overlay. Since the problem
102 * only occurs when scrolling overlays are added, restrict the additional draw to only occur
103 * if scrolling overlays were added since the last draw.
104 */
105 private boolean mAttachedOverlaySinceLastDraw;
Andy Huangbb56a152012-02-24 18:26:47 -0800106
Andy Huang47aa9c92012-07-31 15:37:21 -0700107 private final List<View> mNonScrollingChildren = Lists.newArrayList();
108
Andy Huangbb56a152012-02-24 18:26:47 -0800109 /**
Andy Huang23014702012-07-09 12:50:36 -0700110 * Current document zoom scale per {@link WebView#getScale()}. This is the ratio of actual
111 * screen pixels to logical WebView HTML pixels. We use it to convert from one to the other.
Andy Huangbb56a152012-02-24 18:26:47 -0800112 */
Andy Huangf70fc402012-02-17 15:37:42 -0800113 private float mScale;
Andy Huang120ea662012-03-27 23:15:12 -0700114 /**
115 * Set to true upon receiving the first touch event. Used to help reject invalid WebView scale
116 * values.
117 */
118 private boolean mTouchInitialized;
Andy Huangf70fc402012-02-17 15:37:42 -0800119
Andy Huangbb56a152012-02-24 18:26:47 -0800120 /**
121 * System touch-slop distance per {@link ViewConfiguration#getScaledTouchSlop()}.
122 */
123 private final int mTouchSlop;
124 /**
125 * Current scroll position, as dictated by the background {@link WebView}.
126 */
127 private int mOffsetY;
128 /**
129 * Original pointer Y for slop calculation.
130 */
131 private float mLastMotionY;
132 /**
133 * Original pointer ID for slop calculation.
134 */
135 private int mActivePointerId;
136 /**
137 * Track pointer up/down state to know whether to send a make-up DOWN event to WebView.
138 * WebView internal logic requires that a stream of {@link MotionEvent#ACTION_MOVE} events be
139 * preceded by a {@link MotionEvent#ACTION_DOWN} event.
140 */
141 private boolean mTouchIsDown = false;
142 /**
143 * Remember if touch interception was triggered on a {@link MotionEvent#ACTION_POINTER_DOWN},
144 * so we can send a make-up event in {@link #onTouchEvent(MotionEvent)}.
145 */
146 private boolean mMissedPointerDown;
147
Andy Huang7bdc3752012-03-25 17:18:19 -0700148 /**
Andy Huangc69cee32012-10-07 17:20:39 -0700149 * A recycler that holds removed scrap views, organized by integer item view type. All views
Andrew Sapperstein040b52f2014-04-16 16:37:08 -0700150 * in this data structure should be removed from their view parent prior to insertion.
Andy Huang7bdc3752012-03-25 17:18:19 -0700151 */
152 private final DequeMap<Integer, View> mScrapViews = new DequeMap<Integer, View>();
Andy Huangb5078b22012-03-05 19:52:29 -0800153
154 /**
Andy Huang65fe28f2012-04-06 18:08:53 -0700155 * The current set of overlay views in the view hierarchy. Looking through this map is faster
156 * than traversing the view hierarchy.
Andy Huangb5078b22012-03-05 19:52:29 -0800157 * <p>
158 * WebView sometimes notifies of scroll changes during a draw (or display list generation), when
159 * it's not safe to detach view children because ViewGroup is in the middle of iterating over
Andy Huang65fe28f2012-04-06 18:08:53 -0700160 * its child array. So we remove any child from this list immediately and queue up a task to
161 * detach it later. Since nobody other than the detach task references that view in the
162 * meantime, we don't need any further checks or synchronization.
Andy Huang46dfba62012-04-19 01:47:32 -0700163 * <p>
164 * We keep {@link OverlayView} wrappers instead of bare views so that when it's time to dispose
165 * of all views (on data set or adapter change), we can at least recycle them into the typed
166 * scrap piles for later reuse.
Andy Huangb5078b22012-03-05 19:52:29 -0800167 */
Andy Huang46dfba62012-04-19 01:47:32 -0700168 private final SparseArray<OverlayView> mOverlayViews;
Andy Huangb5078b22012-03-05 19:52:29 -0800169
Andy Huangb5078b22012-03-05 19:52:29 -0800170 private int mWidthMeasureSpec;
171
Andy Huangc7543572012-04-03 15:34:29 -0700172 private boolean mDisableLayoutTracing;
173
Andy Huang31c38a82012-08-15 21:39:43 -0700174 private final InputSmoother mVelocityTracker;
175
Andy Huang46dfba62012-04-19 01:47:32 -0700176 private final DataSetObserver mAdapterObserver = new AdapterObserver();
177
Andy Huangcf5aeae2012-03-09 17:25:08 -0800178 /**
Andy Huang59e0b182012-08-14 14:32:23 -0700179 * The adapter index of the lowest overlay item that is above the top of the screen and reports
180 * {@link ConversationOverlayItem#canPushSnapHeader()}. We calculate this after a pass through
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700181 * {@link #positionOverlays(int, int)}.
Andy Huang59e0b182012-08-14 14:32:23 -0700182 *
183 */
184 private int mSnapIndex;
185
Andy Huang8f187782012-11-06 17:49:25 -0800186 private boolean mSnapEnabled;
187
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700188 /**
189 * A View that fills the remaining vertical space when the overlays do not take
190 * up the entire container. Otherwise, a card-like bottom white space appears.
191 */
Andrew Sapperstein888388c2013-08-01 22:51:37 -0700192 private View mAdditionalBottomBorder;
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700193
194 /**
195 * A flag denoting whether the fake bottom border has been added to the container.
196 */
Andrew Sapperstein888388c2013-08-01 22:51:37 -0700197 private boolean mAdditionalBottomBorderAdded;
198
Andy Huang59e0b182012-08-14 14:32:23 -0700199 /**
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700200 * An int containing the potential top value for the additional bottom border.
201 * If this value is less than the height of the scroll container, the additional
202 * bottom border will be drawn.
203 */
204 private int mAdditionalBottomBorderOverlayTop;
205
206 /**
Andy Huangcf5aeae2012-03-09 17:25:08 -0800207 * Child views of this container should implement this interface to be notified when they are
208 * being detached.
Andy Huangcf5aeae2012-03-09 17:25:08 -0800209 */
210 public interface DetachListener {
211 /**
212 * Called on a child view when it is removed from its parent as part of
213 * {@link ConversationContainer} view recycling.
214 */
215 void onDetachedFromParent();
216 }
217
Andy Huangadbf3e82012-10-13 13:30:19 -0700218 public static class OverlayPosition {
219 public final int top;
220 public final int bottom;
221
222 public OverlayPosition(int top, int bottom) {
223 this.top = top;
224 this.bottom = bottom;
225 }
226 }
227
Andy Huang46dfba62012-04-19 01:47:32 -0700228 private static class OverlayView {
229 public View view;
230 int itemType;
231
232 public OverlayView(View view, int itemType) {
233 this.view = view;
234 this.itemType = itemType;
235 }
236 }
237
Andy Huangf70fc402012-02-17 15:37:42 -0800238 public ConversationContainer(Context c) {
239 this(c, null);
240 }
241
242 public ConversationContainer(Context c, AttributeSet attrs) {
243 super(c, attrs);
Andy Huangbb56a152012-02-24 18:26:47 -0800244
Andy Huang46dfba62012-04-19 01:47:32 -0700245 mOverlayViews = new SparseArray<OverlayView>();
Andy Huangb5078b22012-03-05 19:52:29 -0800246
Andy Huang31c38a82012-08-15 21:39:43 -0700247 mVelocityTracker = new InputSmoother(c);
248
Andy Huangbb56a152012-02-24 18:26:47 -0800249 mTouchSlop = ViewConfiguration.get(c).getScaledTouchSlop();
250
251 // Disabling event splitting fixes pinch-zoom when the first pointer goes down on the
252 // WebView and the second pointer goes down on an overlay view.
253 // Intercepting ACTION_POINTER_DOWN events allows pinch-zoom to work when the first pointer
254 // goes down on an overlay view.
255 setMotionEventSplittingEnabled(false);
256 }
257
258 @Override
259 protected void onFinishInflate() {
260 super.onFinishInflate();
261
Andy Huang5ff63742012-03-16 20:30:23 -0700262 mWebView = (ConversationWebView) findViewById(R.id.webview);
Andy Huangbb56a152012-02-24 18:26:47 -0800263 mWebView.addScrollListener(this);
Andy Huang47aa9c92012-07-31 15:37:21 -0700264
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700265 mTopMostOverlay = findViewById(R.id.conversation_topmost_overlay);
266
Andy Huang47aa9c92012-07-31 15:37:21 -0700267 for (int id : BOTTOM_LAYER_VIEW_IDS) {
268 mNonScrollingChildren.add(findViewById(id));
269 }
270 for (int id : TOP_LAYER_VIEW_IDS) {
271 mNonScrollingChildren.add(findViewById(id));
272 }
Andy Huangf70fc402012-02-17 15:37:42 -0800273 }
274
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700275 public void setupSnapHeader() {
Andrew Sappersteina467d402013-10-15 18:45:03 -0700276 mSnapHeader = (SnapHeader) findViewById(R.id.snap_header);
277 mSnapHeader.setSnappy();
Andrew Sapperstein85ea6182013-10-14 18:21:08 -0700278 }
279
Andrew Sappersteina467d402013-10-15 18:45:03 -0700280 public SnapHeader getSnapHeader() {
Andy Huang59e0b182012-08-14 14:32:23 -0700281 return mSnapHeader;
282 }
283
Andy Huang7bdc3752012-03-25 17:18:19 -0700284 public void setOverlayAdapter(ConversationViewAdapter a) {
Andy Huang46dfba62012-04-19 01:47:32 -0700285 if (mOverlayAdapter != null) {
286 mOverlayAdapter.unregisterDataSetObserver(mAdapterObserver);
287 clearOverlays();
288 }
Andy Huangf70fc402012-02-17 15:37:42 -0800289 mOverlayAdapter = a;
Andy Huang46dfba62012-04-19 01:47:32 -0700290 if (mOverlayAdapter != null) {
291 mOverlayAdapter.registerDataSetObserver(mAdapterObserver);
292 }
Andy Huang51067132012-03-12 20:08:19 -0700293 }
294
295 public Adapter getOverlayAdapter() {
296 return mOverlayAdapter;
Andy Huangf70fc402012-02-17 15:37:42 -0800297 }
298
Andy Huang8f187782012-11-06 17:49:25 -0800299 public void setAccountController(ConversationAccountController controller) {
300 mAccountController = controller;
301
302 mSnapEnabled = isSnapEnabled();
303 }
304
Andy Huang6b3d0d92012-10-30 15:46:48 -0700305 /**
306 * Re-bind any existing views that correspond to the given adapter positions.
307 *
308 */
309 public void onOverlayModelUpdate(List<Integer> affectedAdapterPositions) {
310 for (Integer i : affectedAdapterPositions) {
311 final ConversationOverlayItem item = mOverlayAdapter.getItem(i);
312 final OverlayView overlay = mOverlayViews.get(i);
313 if (overlay != null && overlay.view != null && item != null) {
314 item.onModelUpdated(overlay.view);
315 }
Andy Huang4baafcb2012-11-01 18:01:49 -0700316 // update the snap header too, but only it's showing if the current item
317 if (i == mSnapIndex && mSnapHeader.isBoundTo(item)) {
Andy Huang6b3d0d92012-10-30 15:46:48 -0700318 mSnapHeader.refresh();
319 }
320 }
321 }
322
Andy Huangc1fb9a92013-02-11 13:09:12 -0800323 /**
324 * Return an overlay view for the given adapter item, or null if no matching view is currently
325 * visible. This can happen as you scroll away from an overlay view.
326 *
327 */
328 public View getViewForItem(ConversationOverlayItem item) {
329 View result = null;
330 int adapterPos = -1;
331 for (int i = 0, len = mOverlayAdapter.getCount(); i < len; i++) {
332 if (mOverlayAdapter.getItem(i) == item) {
333 adapterPos = i;
334 break;
335 }
336 }
337 if (adapterPos != -1) {
338 final OverlayView overlay = mOverlayViews.get(adapterPos);
339 if (overlay != null) {
340 result = overlay.view;
341 }
342 }
343 return result;
344 }
345
Andy Huang46dfba62012-04-19 01:47:32 -0700346 private void clearOverlays() {
347 for (int i = 0, len = mOverlayViews.size(); i < len; i++) {
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700348 detachOverlay(mOverlayViews.valueAt(i));
Andy Huang46dfba62012-04-19 01:47:32 -0700349 }
350 mOverlayViews.clear();
351 }
352
353 private void onDataSetChanged() {
Andy Huang2a1e8e32012-10-23 18:54:57 -0700354 // Recycle all views and re-bind them according to the current set of spacer coordinates.
355 // This essentially resets the overlay views and re-renders them.
356 // It's fast enough that it's okay to re-do all views on any small change, as long as
357 // the change isn't too frequent (< ~1Hz).
358
Andy Huang46dfba62012-04-19 01:47:32 -0700359 clearOverlays();
Andy Huang2a1e8e32012-10-23 18:54:57 -0700360 // also unbind the snap header view, so this "reset" causes the snap header to re-create
361 // its view, just like all other headers
362 mSnapHeader.unbind();
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700363
364 // also clear out the additional bottom border
365 removeViewInLayout(mAdditionalBottomBorder);
366 mAdditionalBottomBorderAdded = false;
367
Andy Huang8f187782012-11-06 17:49:25 -0800368 mSnapEnabled = isSnapEnabled();
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700369 positionOverlays(0, mOffsetY);
Andy Huang46dfba62012-04-19 01:47:32 -0700370 }
371
Andy Huangbb56a152012-02-24 18:26:47 -0800372 private void forwardFakeMotionEvent(MotionEvent original, int newAction) {
373 MotionEvent newEvent = MotionEvent.obtain(original);
374 newEvent.setAction(newAction);
375 mWebView.onTouchEvent(newEvent);
376 LogUtils.v(TAG, "in Container.OnTouch. fake: action=%d x/y=%f/%f pointers=%d",
377 newEvent.getActionMasked(), newEvent.getX(), newEvent.getY(),
378 newEvent.getPointerCount());
379 }
380
381 /**
382 * Touch slop code was copied from {@link ScrollView#onInterceptTouchEvent(MotionEvent)}.
383 */
384 @Override
385 public boolean onInterceptTouchEvent(MotionEvent ev) {
Andy Huang120ea662012-03-27 23:15:12 -0700386
387 if (!mTouchInitialized) {
388 mTouchInitialized = true;
389 }
390
Andy Huang632721e2012-04-11 16:57:26 -0700391 // no interception when WebView handles the first DOWN
392 if (mWebView.isHandlingTouch()) {
393 return false;
394 }
395
Andy Huangbb56a152012-02-24 18:26:47 -0800396 boolean intercept = false;
397 switch (ev.getActionMasked()) {
398 case MotionEvent.ACTION_POINTER_DOWN:
Andy Huang632721e2012-04-11 16:57:26 -0700399 LogUtils.d(TAG, "Container is intercepting non-primary touch!");
Andy Huangbb56a152012-02-24 18:26:47 -0800400 intercept = true;
401 mMissedPointerDown = true;
Andy Huang632721e2012-04-11 16:57:26 -0700402 requestDisallowInterceptTouchEvent(true);
Andy Huangbb56a152012-02-24 18:26:47 -0800403 break;
404
405 case MotionEvent.ACTION_DOWN:
406 mLastMotionY = ev.getY();
407 mActivePointerId = ev.getPointerId(0);
408 break;
409
410 case MotionEvent.ACTION_MOVE:
411 final int pointerIndex = ev.findPointerIndex(mActivePointerId);
412 final float y = ev.getY(pointerIndex);
413 final int yDiff = (int) Math.abs(y - mLastMotionY);
414 if (yDiff > mTouchSlop) {
415 mLastMotionY = y;
416 intercept = true;
417 }
418 break;
419 }
420
Andy Huang632721e2012-04-11 16:57:26 -0700421// LogUtils.v(TAG, "in Container.InterceptTouch. action=%d x/y=%f/%f pointers=%d result=%s",
422// ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount(), intercept);
Andy Huangbb56a152012-02-24 18:26:47 -0800423 return intercept;
424 }
425
426 @Override
427 public boolean onTouchEvent(MotionEvent ev) {
428 final int action = ev.getActionMasked();
429
Andy Huang632721e2012-04-11 16:57:26 -0700430 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
Andy Huangbb56a152012-02-24 18:26:47 -0800431 mTouchIsDown = false;
432 } else if (!mTouchIsDown &&
433 (action == MotionEvent.ACTION_MOVE || action == MotionEvent.ACTION_POINTER_DOWN)) {
434
435 forwardFakeMotionEvent(ev, MotionEvent.ACTION_DOWN);
436 if (mMissedPointerDown) {
437 forwardFakeMotionEvent(ev, MotionEvent.ACTION_POINTER_DOWN);
438 mMissedPointerDown = false;
439 }
440
441 mTouchIsDown = true;
442 }
443
444 final boolean webViewResult = mWebView.onTouchEvent(ev);
445
Andy Huang632721e2012-04-11 16:57:26 -0700446// LogUtils.v(TAG, "in Container.OnTouch. action=%d x/y=%f/%f pointers=%d",
447// ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount());
Andy Huangbb56a152012-02-24 18:26:47 -0800448 return webViewResult;
Andy Huangf70fc402012-02-17 15:37:42 -0800449 }
450
451 @Override
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700452 public void onNotifierScroll(final int x, final int y) {
Andy Huang31c38a82012-08-15 21:39:43 -0700453 mVelocityTracker.onInput(y);
Andy Huangc7543572012-04-03 15:34:29 -0700454 mDisableLayoutTracing = true;
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700455 positionOverlays(x, y);
Andy Huangc7543572012-04-03 15:34:29 -0700456 mDisableLayoutTracing = false;
Andy Huangb5078b22012-03-05 19:52:29 -0800457 }
458
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700459 private void positionOverlays(int x, int y) {
Andy Huangf70fc402012-02-17 15:37:42 -0800460 mOffsetY = y;
Andy Huang07f87322012-03-27 18:03:53 -0700461
462 /*
Andy Huang120ea662012-03-27 23:15:12 -0700463 * The scale value that WebView reports is inaccurate when measured during WebView
464 * initialization. This bug is present in ICS, so to work around it, we ignore all
Andy Huang23014702012-07-09 12:50:36 -0700465 * reported values and use a calculated expected value from ConversationWebView instead.
466 * Only when the user actually begins to touch the view (to, say, begin a zoom) do we begin
467 * to pay attention to WebView-reported scale values.
Andy Huang07f87322012-03-27 18:03:53 -0700468 */
Andy Huang120ea662012-03-27 23:15:12 -0700469 if (mTouchInitialized) {
Andy Huang07f87322012-03-27 18:03:53 -0700470 mScale = mWebView.getScale();
Andy Huang23014702012-07-09 12:50:36 -0700471 } else if (mScale == 0) {
472 mScale = mWebView.getInitialScale();
Andy Huang07f87322012-03-27 18:03:53 -0700473 }
Andy Huangc7543572012-04-03 15:34:29 -0700474 traceLayout("in positionOverlays, raw scale=%f, effective scale=%f", mWebView.getScale(),
475 mScale);
Andy Huangf70fc402012-02-17 15:37:42 -0800476
Andy Huangadbf3e82012-10-13 13:30:19 -0700477 if (mOverlayPositions == null || mOverlayAdapter == null) {
Andy Huang51067132012-03-12 20:08:19 -0700478 return;
479 }
480
Andy Huangb5078b22012-03-05 19:52:29 -0800481 // recycle scrolled-off views and add newly visible views
Andy Huangb5078b22012-03-05 19:52:29 -0800482
Andy Huang7bdc3752012-03-25 17:18:19 -0700483 // we want consecutive spacers/overlays to stack towards the bottom
484 // so iterate from the bottom of the conversation up
485 // starting with the last spacer bottom and the last adapter item, position adapter views
486 // in a single stack until you encounter a non-contiguous expanded message header,
487 // then decrement to the next spacer.
488
Andy Huangadbf3e82012-10-13 13:30:19 -0700489 traceLayout("IN positionOverlays, spacerCount=%d overlayCount=%d", mOverlayPositions.length,
Andy Huangc7543572012-04-03 15:34:29 -0700490 mOverlayAdapter.getCount());
491
Andy Huang59e0b182012-08-14 14:32:23 -0700492 mSnapIndex = -1;
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700493 mAdditionalBottomBorderOverlayTop = 0;
Andy Huang59e0b182012-08-14 14:32:23 -0700494
Andy Huangadbf3e82012-10-13 13:30:19 -0700495 int adapterLoopIndex = mOverlayAdapter.getCount() - 1;
496 int spacerIndex = mOverlayPositions.length - 1;
497 while (spacerIndex >= 0 && adapterLoopIndex >= 0) {
Andy Huangc7543572012-04-03 15:34:29 -0700498
Andy Huangadbf3e82012-10-13 13:30:19 -0700499 final int spacerTop = getOverlayTop(spacerIndex);
500 final int spacerBottom = getOverlayBottom(spacerIndex);
501
502 final boolean flip;
503 final int flipOffset;
504 final int forceGravity;
505 // flip direction from bottom->top to top->bottom traversal on the very first spacer
506 // to facilitate top-aligned headers at spacer index = 0
507 if (spacerIndex == 0) {
508 flip = true;
509 flipOffset = adapterLoopIndex;
510 forceGravity = Gravity.TOP;
511 } else {
512 flip = false;
513 flipOffset = 0;
514 forceGravity = Gravity.NO_GRAVITY;
515 }
516
517 int adapterIndex = flip ? flipOffset - adapterLoopIndex : adapterLoopIndex;
Andy Huang7bdc3752012-03-25 17:18:19 -0700518
519 // always place at least one overlay per spacer
Andy Huang46dfba62012-04-19 01:47:32 -0700520 ConversationOverlayItem adapterItem = mOverlayAdapter.getItem(adapterIndex);
Andy Huang7bdc3752012-03-25 17:18:19 -0700521
Andy Huangadbf3e82012-10-13 13:30:19 -0700522 OverlayPosition itemPos = calculatePosition(adapterItem, spacerTop, spacerBottom,
523 forceGravity);
Andy Huang7bdc3752012-03-25 17:18:19 -0700524
Andy Huangc7543572012-04-03 15:34:29 -0700525 traceLayout("in loop, spacer=%d overlay=%d t/b=%d/%d (%s)", spacerIndex, adapterIndex,
Andy Huangadbf3e82012-10-13 13:30:19 -0700526 itemPos.top, itemPos.bottom, adapterItem);
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700527 positionOverlay(adapterIndex, itemPos.top, itemPos.bottom);
Andy Huang7bdc3752012-03-25 17:18:19 -0700528
Andy Huangadbf3e82012-10-13 13:30:19 -0700529 // and keep stacking overlays unconditionally if we are on the first spacer, or as long
530 // as overlays are contiguous
531 while (--adapterLoopIndex >= 0) {
532 adapterIndex = flip ? flipOffset - adapterLoopIndex : adapterLoopIndex;
Andy Huang7bdc3752012-03-25 17:18:19 -0700533 adapterItem = mOverlayAdapter.getItem(adapterIndex);
Andy Huangadbf3e82012-10-13 13:30:19 -0700534 if (spacerIndex > 0 && !adapterItem.isContiguous()) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700535 // advance to the next spacer, but stay on this adapter item
536 break;
Andy Huangb5078b22012-03-05 19:52:29 -0800537 }
Andy Huangb5078b22012-03-05 19:52:29 -0800538
Andy Huangadbf3e82012-10-13 13:30:19 -0700539 // place this overlay in the region of the spacer above or below the last item,
540 // depending on direction of iteration
541 final int regionTop = flip ? itemPos.bottom : spacerTop;
542 final int regionBottom = flip ? spacerBottom : itemPos.top;
543 itemPos = calculatePosition(adapterItem, regionTop, regionBottom, forceGravity);
Andy Huangb5078b22012-03-05 19:52:29 -0800544
Andy Huangc7543572012-04-03 15:34:29 -0700545 traceLayout("in contig loop, spacer=%d overlay=%d t/b=%d/%d (%s)", spacerIndex,
Andy Huangadbf3e82012-10-13 13:30:19 -0700546 adapterIndex, itemPos.top, itemPos.bottom, adapterItem);
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700547 positionOverlay(adapterIndex, itemPos.top, itemPos.bottom);
Andy Huangb5078b22012-03-05 19:52:29 -0800548 }
Andy Huangc7543572012-04-03 15:34:29 -0700549
550 spacerIndex--;
Andy Huangb5078b22012-03-05 19:52:29 -0800551 }
Andy Huang59e0b182012-08-14 14:32:23 -0700552
Andy Huang31c38a82012-08-15 21:39:43 -0700553 positionSnapHeader(mSnapIndex);
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700554 positionAdditionalBottomBorder();
Andy Huangb5078b22012-03-05 19:52:29 -0800555 }
556
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700557 /**
558 * Adds an additional bottom border to the overlay views in case
559 * the overlays do not fill the entire screen.
560 */
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700561 private void positionAdditionalBottomBorder() {
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700562 final int lastBottom = mAdditionalBottomBorderOverlayTop;
Andrew Sapperstein83b460b2013-08-23 15:33:43 -0700563 final int containerHeight = webPxToScreenPx(mWebView.getContentHeight());
Andrew Sapperstein888388c2013-08-01 22:51:37 -0700564 final int speculativeHeight = containerHeight - lastBottom;
565 if (speculativeHeight > 0) {
566 if (mAdditionalBottomBorder == null) {
567 mAdditionalBottomBorder = mOverlayAdapter.getLayoutInflater().inflate(
568 R.layout.fake_bottom_border, this, false);
569 }
570
571 setAdditionalBottomBorderHeight(speculativeHeight);
572
573 if (!mAdditionalBottomBorderAdded) {
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700574 addViewInLayoutWrapper(mAdditionalBottomBorder);
Andrew Sapperstein888388c2013-08-01 22:51:37 -0700575 mAdditionalBottomBorderAdded = true;
576 }
577
578 measureOverlayView(mAdditionalBottomBorder);
579 layoutOverlay(mAdditionalBottomBorder, lastBottom, containerHeight);
580 } else {
581 if (mAdditionalBottomBorder != null && mAdditionalBottomBorderAdded) {
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700582 removeViewInLayout(mAdditionalBottomBorder);
583 mAdditionalBottomBorderAdded = false;
Andrew Sapperstein888388c2013-08-01 22:51:37 -0700584 }
585 }
586 }
587
588 private void setAdditionalBottomBorderHeight(int speculativeHeight) {
589 LayoutParams params = mAdditionalBottomBorder.getLayoutParams();
590 params.height = speculativeHeight;
591 mAdditionalBottomBorder.setLayoutParams(params);
592 }
593
Scott Kennedyff8553f2013-04-05 20:57:44 -0700594 private static OverlayPosition calculatePosition(final ConversationOverlayItem adapterItem,
595 final int withinTop, final int withinBottom, final int forceGravity) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700596 if (adapterItem.getHeight() == 0) {
597 // "place" invisible items at the bottom of their region to stay consistent with the
598 // stacking algorithm in positionOverlays(), unless gravity is forced to the top
599 final int y = (forceGravity == Gravity.TOP) ? withinTop : withinBottom;
600 return new OverlayPosition(y, y);
601 }
602
603 final int v = ((forceGravity != Gravity.NO_GRAVITY) ?
604 forceGravity : adapterItem.getGravity()) & Gravity.VERTICAL_GRAVITY_MASK;
605 switch (v) {
606 case Gravity.BOTTOM:
607 return new OverlayPosition(withinBottom - adapterItem.getHeight(), withinBottom);
608 case Gravity.TOP:
609 return new OverlayPosition(withinTop, withinTop + adapterItem.getHeight());
610 default:
611 throw new UnsupportedOperationException("unsupported gravity: " + v);
612 }
613 }
614
Andy Huangb5078b22012-03-05 19:52:29 -0800615 /**
Andy Huang9875bb42012-04-04 20:36:21 -0700616 * Executes a measure pass over the specified child overlay view and returns the measured
617 * height. The measurement uses whatever the current container's width measure spec is.
618 * This method ignores view visibility and returns the height that the view would be if visible.
619 *
620 * @param overlayView an overlay view to measure. does not actually have to be attached yet.
621 * @return height that the view would be if it was visible
622 */
623 public int measureOverlay(View overlayView) {
624 measureOverlayView(overlayView);
625 return overlayView.getMeasuredHeight();
626 }
627
628 /**
Andy Huangb5078b22012-03-05 19:52:29 -0800629 * Copied/stolen from {@link ListView}.
630 */
Andy Huang9875bb42012-04-04 20:36:21 -0700631 private void measureOverlayView(View child) {
Andy Huang47aa9c92012-07-31 15:37:21 -0700632 MarginLayoutParams p = (MarginLayoutParams) child.getLayoutParams();
Andy Huangb5078b22012-03-05 19:52:29 -0800633 if (p == null) {
Andy Huang47aa9c92012-07-31 15:37:21 -0700634 p = (MarginLayoutParams) generateDefaultLayoutParams();
Andy Huangb5078b22012-03-05 19:52:29 -0800635 }
636
637 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
Andy Huang47aa9c92012-07-31 15:37:21 -0700638 getPaddingLeft() + getPaddingRight() + p.leftMargin + p.rightMargin, p.width);
Andy Huangb5078b22012-03-05 19:52:29 -0800639 int lpHeight = p.height;
640 int childHeightSpec;
641 if (lpHeight > 0) {
642 childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
643 } else {
644 childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
645 }
646 child.measure(childWidthSpec, childHeightSpec);
647 }
648
Andy Huang46dfba62012-04-19 01:47:32 -0700649 private void onOverlayScrolledOff(final int adapterIndex, final OverlayView overlay,
650 int overlayTop, int overlayBottom) {
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700651 // detach the view asynchronously, as scroll notification can happen during a draw, when
652 // it's not safe to remove children
653
654 // but immediately remove this view from the view set so future lookups don't find it
Andy Huang65fe28f2012-04-06 18:08:53 -0700655 mOverlayViews.remove(adapterIndex);
Andy Huangb5078b22012-03-05 19:52:29 -0800656
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700657 post(new Runnable() {
658 @Override
659 public void run() {
660 detachOverlay(overlay);
661 }
662 });
Andy Huangb5078b22012-03-05 19:52:29 -0800663
664 // push it out of view immediately
665 // otherwise this scrolled-off header will continue to draw until the runnable runs
Andy Huang46dfba62012-04-19 01:47:32 -0700666 layoutOverlay(overlay.view, overlayTop, overlayBottom);
Andy Huangb5078b22012-03-05 19:52:29 -0800667 }
668
Andy Huang8fdce822012-08-27 15:00:45 -0700669 /**
Andy Huangc69cee32012-10-07 17:20:39 -0700670 * Returns an existing scrap view, if available. The view will already be removed from the view
Andy Huang8fdce822012-08-27 15:00:45 -0700671 * hierarchy. This method will not remove the view from the scrap heap.
672 *
673 */
Andy Huang7bdc3752012-03-25 17:18:19 -0700674 public View getScrapView(int type) {
Andy Huang8fdce822012-08-27 15:00:45 -0700675 return mScrapViews.peek(type);
Andy Huang7bdc3752012-03-25 17:18:19 -0700676 }
677
678 public void addScrapView(int type, View v) {
679 mScrapViews.add(type, v);
680 }
681
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700682 private void detachOverlay(OverlayView overlay) {
Andy Huangc69cee32012-10-07 17:20:39 -0700683 // Prefer removeViewInLayout over removeView. The typical followup layout pass is unneeded
684 // because removing overlay views doesn't affect overall layout.
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700685 removeViewInLayout(overlay.view);
Andy Huang46dfba62012-04-19 01:47:32 -0700686 mScrapViews.add(overlay.itemType, overlay.view);
687 if (overlay.view instanceof DetachListener) {
688 ((DetachListener) overlay.view).onDetachedFromParent();
Andy Huangcf5aeae2012-03-09 17:25:08 -0800689 }
690 }
691
692 @Override
Andy Huangf70fc402012-02-17 15:37:42 -0800693 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Andy Huangf70fc402012-02-17 15:37:42 -0800694 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Andy Huang632721e2012-04-11 16:57:26 -0700695 if (LogUtils.isLoggable(TAG, LogUtils.DEBUG)) {
696 LogUtils.d(TAG, "*** IN header container onMeasure spec for w/h=%s/%s",
697 MeasureSpec.toString(widthMeasureSpec),
698 MeasureSpec.toString(heightMeasureSpec));
699 }
Andy Huangf70fc402012-02-17 15:37:42 -0800700
Andy Huang47aa9c92012-07-31 15:37:21 -0700701 for (View nonScrollingChild : mNonScrollingChildren) {
702 if (nonScrollingChild.getVisibility() != GONE) {
703 measureChildWithMargins(nonScrollingChild, widthMeasureSpec, 0 /* widthUsed */,
704 heightMeasureSpec, 0 /* heightUsed */);
705 }
Andy Huang3233bff2012-03-20 19:38:45 -0700706 }
Andy Huangb5078b22012-03-05 19:52:29 -0800707 mWidthMeasureSpec = widthMeasureSpec;
Andy Huang7bdc3752012-03-25 17:18:19 -0700708
Andy Huang9875bb42012-04-04 20:36:21 -0700709 // onLayout will re-measure and re-position overlays for the new container size, but the
710 // spacer offsets would still need to be updated to have them draw at their new locations.
Andy Huangf70fc402012-02-17 15:37:42 -0800711 }
712
713 @Override
714 protected void onLayout(boolean changed, int l, int t, int r, int b) {
Andy Huang632721e2012-04-11 16:57:26 -0700715 LogUtils.d(TAG, "*** IN header container onLayout");
Andy Huangf70fc402012-02-17 15:37:42 -0800716
Andy Huang47aa9c92012-07-31 15:37:21 -0700717 for (View nonScrollingChild : mNonScrollingChildren) {
718 if (nonScrollingChild.getVisibility() != GONE) {
719 final int w = nonScrollingChild.getMeasuredWidth();
720 final int h = nonScrollingChild.getMeasuredHeight();
721
722 final MarginLayoutParams lp =
723 (MarginLayoutParams) nonScrollingChild.getLayoutParams();
724
725 final int childLeft = lp.leftMargin;
726 final int childTop = lp.topMargin;
727 nonScrollingChild.layout(childLeft, childTop, childLeft + w, childTop + h);
728 }
729 }
Andy Huangf70fc402012-02-17 15:37:42 -0800730
Andy Huange20e1632012-06-15 15:59:07 -0700731 if (mOverlayAdapter != null) {
732 // being in a layout pass means overlay children may require measurement,
733 // so invalidate them
734 for (int i = 0, len = mOverlayAdapter.getCount(); i < len; i++) {
735 mOverlayAdapter.getItem(i).invalidateMeasurement();
736 }
Andy Huang8778a872012-04-20 17:50:41 -0700737 }
738
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700739 positionOverlays(0, mOffsetY);
740 }
741
742 @Override
743 protected void dispatchDraw(Canvas canvas) {
744 super.dispatchDraw(canvas);
745
746 if (mAttachedOverlaySinceLastDraw) {
747 drawChild(canvas, mTopMostOverlay, getDrawingTime());
748 mAttachedOverlaySinceLastDraw = false;
749 }
Andy Huang59e0b182012-08-14 14:32:23 -0700750 }
751
752 @Override
Andy Huang47aa9c92012-07-31 15:37:21 -0700753 protected LayoutParams generateDefaultLayoutParams() {
754 return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
755 }
756
757 @Override
758 public LayoutParams generateLayoutParams(AttributeSet attrs) {
759 return new MarginLayoutParams(getContext(), attrs);
760 }
761
762 @Override
763 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
764 return new MarginLayoutParams(p);
765 }
766
767 @Override
768 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
769 return p instanceof MarginLayoutParams;
770 }
771
Andy Huangadbf3e82012-10-13 13:30:19 -0700772 private int getOverlayTop(int spacerIndex) {
773 return webPxToScreenPx(mOverlayPositions[spacerIndex].top);
774 }
775
Andy Huang7bdc3752012-03-25 17:18:19 -0700776 private int getOverlayBottom(int spacerIndex) {
Andy Huangadbf3e82012-10-13 13:30:19 -0700777 return webPxToScreenPx(mOverlayPositions[spacerIndex].bottom);
778 }
779
780 private int webPxToScreenPx(int webPx) {
Andy Huangb5078b22012-03-05 19:52:29 -0800781 // TODO: round or truncate?
Andy Huangadbf3e82012-10-13 13:30:19 -0700782 // TODO: refactor and unify with ConversationWebView.webPxToScreenPx()
783 return (int) (webPx * mScale);
Andy Huang7bdc3752012-03-25 17:18:19 -0700784 }
785
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700786 private void positionOverlay(int adapterIndex, int overlayTopY, int overlayBottomY) {
Andy Huang46dfba62012-04-19 01:47:32 -0700787 final OverlayView overlay = mOverlayViews.get(adapterIndex);
788 final ConversationOverlayItem item = mOverlayAdapter.getItem(adapterIndex);
Andy Huang9875bb42012-04-04 20:36:21 -0700789
Andy Huang31c38a82012-08-15 21:39:43 -0700790 // save off the item's current top for later snap calculations
791 item.setTop(overlayTopY);
792
Andy Huangc7543572012-04-03 15:34:29 -0700793 // is the overlay visible and does it have non-zero height?
794 if (overlayTopY != overlayBottomY && overlayBottomY > mOffsetY
795 && overlayTopY < mOffsetY + getHeight()) {
Andy Huang46dfba62012-04-19 01:47:32 -0700796 View overlayView = overlay != null ? overlay.view : null;
Andy Huang7bdc3752012-03-25 17:18:19 -0700797 // show and/or move overlay
798 if (overlayView == null) {
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700799 overlayView = addOverlayView(adapterIndex);
Andrew Sapperstein2fd167d2014-01-28 10:07:38 -0800800 ViewCompat.setLayoutDirection(overlayView, ViewCompat.getLayoutDirection(this));
Andy Huang9875bb42012-04-04 20:36:21 -0700801 measureOverlayView(overlayView);
802 item.markMeasurementValid();
803 traceLayout("show/measure overlay %d", adapterIndex);
Andy Huangc7543572012-04-03 15:34:29 -0700804 } else {
805 traceLayout("move overlay %d", adapterIndex);
Andy Huang9875bb42012-04-04 20:36:21 -0700806 if (!item.isMeasurementValid()) {
Andrew Sappersteincee3c902013-07-31 10:52:02 -0700807 item.rebindView(overlayView);
Andy Huang9875bb42012-04-04 20:36:21 -0700808 measureOverlayView(overlayView);
809 item.markMeasurementValid();
810 traceLayout("and (re)measure overlay %d, old/new heights=%d/%d", adapterIndex,
811 overlayView.getHeight(), overlayView.getMeasuredHeight());
812 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700813 }
Andy Huang9875bb42012-04-04 20:36:21 -0700814 traceLayout("laying out overlay %d with h=%d", adapterIndex,
815 overlayView.getMeasuredHeight());
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700816 final int childBottom = overlayTopY + overlayView.getMeasuredHeight();
817 layoutOverlay(overlayView, overlayTopY, childBottom);
818 mAdditionalBottomBorderOverlayTop = (childBottom > mAdditionalBottomBorderOverlayTop) ?
819 childBottom : mAdditionalBottomBorderOverlayTop;
Andy Huang7bdc3752012-03-25 17:18:19 -0700820 } else {
821 // hide overlay
Andy Huang46dfba62012-04-19 01:47:32 -0700822 if (overlay != null) {
Andy Huangc7543572012-04-03 15:34:29 -0700823 traceLayout("hide overlay %d", adapterIndex);
Andy Huang46dfba62012-04-19 01:47:32 -0700824 onOverlayScrolledOff(adapterIndex, overlay, overlayTopY, overlayBottomY);
Andy Huangc7543572012-04-03 15:34:29 -0700825 } else {
826 traceLayout("ignore non-visible overlay %d", adapterIndex);
Andy Huang7bdc3752012-03-25 17:18:19 -0700827 }
Andrew Sappersteinef6367d2013-08-04 15:43:46 -0700828 mAdditionalBottomBorderOverlayTop = (overlayBottomY > mAdditionalBottomBorderOverlayTop)
829 ? overlayBottomY : mAdditionalBottomBorderOverlayTop;
Andy Huang7bdc3752012-03-25 17:18:19 -0700830 }
Andy Huang59e0b182012-08-14 14:32:23 -0700831
832 if (overlayTopY <= mOffsetY && item.canPushSnapHeader()) {
833 if (mSnapIndex == -1) {
834 mSnapIndex = adapterIndex;
835 } else if (adapterIndex > mSnapIndex) {
836 mSnapIndex = adapterIndex;
837 }
838 }
839
Andy Huang7bdc3752012-03-25 17:18:19 -0700840 }
841
842 // layout an existing view
843 // need its top offset into the conversation, its height, and the scroll offset
Andy Huangc7543572012-04-03 15:34:29 -0700844 private void layoutOverlay(View child, int childTop, int childBottom) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700845 final int top = childTop - mOffsetY;
Andy Huangc7543572012-04-03 15:34:29 -0700846 final int bottom = childBottom - mOffsetY;
Andy Huang47aa9c92012-07-31 15:37:21 -0700847
848 final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
849 final int childLeft = getPaddingLeft() + lp.leftMargin;
850
851 child.layout(childLeft, top, childLeft + child.getMeasuredWidth(), bottom);
Andy Huangb5078b22012-03-05 19:52:29 -0800852 }
853
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700854 private View addOverlayView(int adapterIndex) {
Andy Huang7bdc3752012-03-25 17:18:19 -0700855 final int itemType = mOverlayAdapter.getItemViewType(adapterIndex);
856 final View convertView = mScrapViews.poll(itemType);
Andy Huangf70fc402012-02-17 15:37:42 -0800857
Andy Huang256b35c2012-08-22 15:19:13 -0700858 final View view = mOverlayAdapter.getView(adapterIndex, convertView, this);
Andy Huang46dfba62012-04-19 01:47:32 -0700859 mOverlayViews.put(adapterIndex, new OverlayView(view, itemType));
Andy Huang7bdc3752012-03-25 17:18:19 -0700860
Andy Huangc69cee32012-10-07 17:20:39 -0700861 if (convertView == view) {
Andy Huangc7543572012-04-03 15:34:29 -0700862 LogUtils.d(TAG, "want to REUSE scrolled-in view: index=%d obj=%s", adapterIndex, view);
Andy Huang7bdc3752012-03-25 17:18:19 -0700863 } else {
Andy Huangc7543572012-04-03 15:34:29 -0700864 LogUtils.d(TAG, "want to CREATE scrolled-in view: index=%d obj=%s", adapterIndex, view);
Andy Huangf70fc402012-02-17 15:37:42 -0800865 }
Andy Huang7bdc3752012-03-25 17:18:19 -0700866
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700867 addViewInLayoutWrapper(view);
Andy Huang59e0b182012-08-14 14:32:23 -0700868
Andy Huang7bdc3752012-03-25 17:18:19 -0700869 return view;
Andy Huangf70fc402012-02-17 15:37:42 -0800870 }
871
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700872 private void addViewInLayoutWrapper(View view) {
873 final int index = BOTTOM_LAYER_VIEW_IDS.length;
874 addViewInLayout(view, index, view.getLayoutParams(), true /* preventRequestLayout */);
875 mAttachedOverlaySinceLastDraw = true;
Andrew Sapperstein888388c2013-08-01 22:51:37 -0700876 }
877
Andy Huang8f187782012-11-06 17:49:25 -0800878 private boolean isSnapEnabled() {
879 if (mAccountController == null || mAccountController.getAccount() == null
880 || mAccountController.getAccount().settings == null) {
881 return true;
882 }
883 final int snap = mAccountController.getAccount().settings.snapHeaders;
884 return snap == UIProvider.SnapHeaderValue.ALWAYS ||
885 (snap == UIProvider.SnapHeaderValue.PORTRAIT_ONLY && getResources()
886 .getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);
887 }
888
Andy Huang31c38a82012-08-15 21:39:43 -0700889 // render and/or re-position snap header
890 private void positionSnapHeader(int snapIndex) {
891 ConversationOverlayItem snapItem = null;
Andy Huang8f187782012-11-06 17:49:25 -0800892 if (mSnapEnabled && snapIndex != -1) {
Andy Huang31c38a82012-08-15 21:39:43 -0700893 final ConversationOverlayItem item = mOverlayAdapter.getItem(snapIndex);
894 if (item.canBecomeSnapHeader()) {
895 snapItem = item;
896 }
897 }
898 if (snapItem == null) {
899 mSnapHeader.setVisibility(GONE);
900 mSnapHeader.unbind();
901 return;
902 }
903
904 snapItem.bindView(mSnapHeader, false /* measureOnly */);
905 mSnapHeader.setVisibility(VISIBLE);
906
Andy Huang0a648c92012-10-02 14:56:46 -0700907 // overlap is negative or zero; bump the snap header upwards by that much
Andy Huang31c38a82012-08-15 21:39:43 -0700908 int overlap = 0;
909
910 final ConversationOverlayItem next = findNextPushingOverlay(snapIndex + 1);
911 if (next != null) {
912 overlap = Math.min(0, next.getTop() - mSnapHeader.getHeight() - mOffsetY);
913
914 // disable overlap drawing past a certain speed
915 if (overlap < 0) {
916 final Float v = mVelocityTracker.getSmoothedVelocity();
917 if (v != null && v > SNAP_HEADER_MAX_SCROLL_SPEED) {
918 overlap = 0;
919 }
920 }
921 }
Andy Huang0a648c92012-10-02 14:56:46 -0700922
923 mSnapHeader.setTranslationY(overlap);
Andy Huang31c38a82012-08-15 21:39:43 -0700924 }
925
926 // find the next header that can push the snap header up
927 private ConversationOverlayItem findNextPushingOverlay(int start) {
Andy Huang31c38a82012-08-15 21:39:43 -0700928 for (int i = start, len = mOverlayAdapter.getCount(); i < len; i++) {
929 final ConversationOverlayItem next = mOverlayAdapter.getItem(i);
930 if (next.canPushSnapHeader()) {
931 return next;
932 }
933 }
934 return null;
935 }
936
Andy Huangc7543572012-04-03 15:34:29 -0700937 /**
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700938 * Return a collection of all currently visible overlay views, in no particular order.
939 * Please don't mess with them too badly (e.g. remove from parent).
940 *
941 */
942 public List<View> getOverlayViews() {
943 final List<View> views = Lists.newArrayList();
944 for (int i = 0, len = mOverlayViews.size(); i < len; i++) {
945 views.add(mOverlayViews.valueAt(i).view);
946 }
947 return views;
948 }
949
950 /**
Andy Huangadbf3e82012-10-13 13:30:19 -0700951 * Prevents any layouts from happening until the next time
952 * {@link #onGeometryChange(OverlayPosition[])} is
Andy Huangc7543572012-04-03 15:34:29 -0700953 * called. Useful when you know the HTML spacer coordinates are inconsistent with adapter items.
954 * <p>
Andy Huangadbf3e82012-10-13 13:30:19 -0700955 * If you call this, you must ensure that a followup call to
956 * {@link #onGeometryChange(OverlayPosition[])}
Andy Huangc7543572012-04-03 15:34:29 -0700957 * is made later, when the HTML spacer coordinates are updated.
958 *
959 */
960 public void invalidateSpacerGeometry() {
Andy Huangadbf3e82012-10-13 13:30:19 -0700961 mOverlayPositions = null;
Andy Huangc7543572012-04-03 15:34:29 -0700962 }
963
Andy Huangadbf3e82012-10-13 13:30:19 -0700964 public void onGeometryChange(OverlayPosition[] overlayPositions) {
965 traceLayout("*** got overlay spacer positions:");
966 for (OverlayPosition pos : overlayPositions) {
967 traceLayout("top=%d bottom=%d", pos.top, pos.bottom);
Andy Huangf70fc402012-02-17 15:37:42 -0800968 }
969
Andy Huangadbf3e82012-10-13 13:30:19 -0700970 mOverlayPositions = overlayPositions;
Andrew Sappersteind97eb872014-04-16 16:48:03 -0700971 positionOverlays(0, mOffsetY);
Andy Huangf70fc402012-02-17 15:37:42 -0800972 }
973
Andy Huangc7543572012-04-03 15:34:29 -0700974 private void traceLayout(String msg, Object... params) {
975 if (mDisableLayoutTracing) {
976 return;
977 }
Andy Huang7f9ef602012-07-25 16:44:30 -0700978 LogUtils.d(TAG, msg, params);
Andy Huangc7543572012-04-03 15:34:29 -0700979 }
980
Andy Huang46dfba62012-04-19 01:47:32 -0700981 private class AdapterObserver extends DataSetObserver {
982 @Override
983 public void onChanged() {
984 onDataSetChanged();
985 }
986 }
Andy Huangf70fc402012-02-17 15:37:42 -0800987}