Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 1 | /* |
| 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 Huang | 5ff6374 | 2012-03-16 20:30:23 -0700 | [diff] [blame] | 18 | package com.android.mail.browse; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 19 | |
| 20 | import android.content.Context; |
Andy Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 21 | import android.content.res.Configuration; |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 22 | import android.database.DataSetObserver; |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 23 | import android.graphics.Canvas; |
Andrew Sapperstein | 2fd167d | 2014-01-28 10:07:38 -0800 | [diff] [blame] | 24 | import android.support.v4.view.ViewCompat; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 25 | import android.util.AttributeSet; |
Andy Huang | 65fe28f | 2012-04-06 18:08:53 -0700 | [diff] [blame] | 26 | import android.util.SparseArray; |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 27 | import android.view.Gravity; |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 28 | import android.view.MotionEvent; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 29 | import android.view.View; |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 30 | import android.view.ViewConfiguration; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 31 | import android.view.ViewGroup; |
| 32 | import android.webkit.WebView; |
| 33 | import android.widget.Adapter; |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 34 | import android.widget.ListView; |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 35 | import android.widget.ScrollView; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 36 | |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 37 | import com.android.mail.R; |
Andy Huang | 5ff6374 | 2012-03-16 20:30:23 -0700 | [diff] [blame] | 38 | import com.android.mail.browse.ScrollNotifier.ScrollListener; |
Andy Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 39 | import com.android.mail.providers.UIProvider; |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 40 | import com.android.mail.ui.ConversationViewFragment; |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 41 | import com.android.mail.utils.DequeMap; |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 42 | import com.android.mail.utils.InputSmoother; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 43 | import com.android.mail.utils.LogUtils; |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 44 | import com.google.common.collect.Lists; |
| 45 | |
| 46 | import java.util.List; |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 47 | |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 48 | /** |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 49 | * 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 Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 66 | * ListView and AbsListView. |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 67 | * |
| 68 | */ |
| 69 | public class ConversationContainer extends ViewGroup implements ScrollListener { |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 70 | private static final String TAG = ConversationViewFragment.LAYOUT_TAG; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 71 | |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 72 | private static final int[] BOTTOM_LAYER_VIEW_IDS = { |
Andrew Sapperstein | 14f9374 | 2013-07-25 14:29:56 -0700 | [diff] [blame] | 73 | R.id.webview, |
| 74 | R.id.conversation_side_border_overlay |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 75 | }; |
| 76 | |
| 77 | private static final int[] TOP_LAYER_VIEW_IDS = { |
| 78 | R.id.conversation_topmost_overlay |
| 79 | }; |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 80 | |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 81 | /** |
| 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 Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 87 | private ConversationAccountController mAccountController; |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 88 | private ConversationViewAdapter mOverlayAdapter; |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 89 | private OverlayPosition[] mOverlayPositions; |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 90 | private ConversationWebView mWebView; |
Andrew Sapperstein | a467d40 | 2013-10-15 18:45:03 -0700 | [diff] [blame] | 91 | private SnapHeader mSnapHeader; |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 92 | 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 Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 106 | |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 107 | private final List<View> mNonScrollingChildren = Lists.newArrayList(); |
| 108 | |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 109 | /** |
Andy Huang | 2301470 | 2012-07-09 12:50:36 -0700 | [diff] [blame] | 110 | * 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 Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 112 | */ |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 113 | private float mScale; |
Andy Huang | 120ea66 | 2012-03-27 23:15:12 -0700 | [diff] [blame] | 114 | /** |
| 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 Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 119 | |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 120 | /** |
| 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 Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 148 | /** |
Andy Huang | c69cee3 | 2012-10-07 17:20:39 -0700 | [diff] [blame] | 149 | * A recycler that holds removed scrap views, organized by integer item view type. All views |
Andrew Sapperstein | 040b52f | 2014-04-16 16:37:08 -0700 | [diff] [blame] | 150 | * in this data structure should be removed from their view parent prior to insertion. |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 151 | */ |
| 152 | private final DequeMap<Integer, View> mScrapViews = new DequeMap<Integer, View>(); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 153 | |
| 154 | /** |
Andy Huang | 65fe28f | 2012-04-06 18:08:53 -0700 | [diff] [blame] | 155 | * The current set of overlay views in the view hierarchy. Looking through this map is faster |
| 156 | * than traversing the view hierarchy. |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 157 | * <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 Huang | 65fe28f | 2012-04-06 18:08:53 -0700 | [diff] [blame] | 160 | * 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 Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 163 | * <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 Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 167 | */ |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 168 | private final SparseArray<OverlayView> mOverlayViews; |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 169 | |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 170 | private int mWidthMeasureSpec; |
| 171 | |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 172 | private boolean mDisableLayoutTracing; |
| 173 | |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 174 | private final InputSmoother mVelocityTracker; |
| 175 | |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 176 | private final DataSetObserver mAdapterObserver = new AdapterObserver(); |
| 177 | |
Andy Huang | cf5aeae | 2012-03-09 17:25:08 -0800 | [diff] [blame] | 178 | /** |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 179 | * 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 Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 181 | * {@link #positionOverlays(int, int)}. |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 182 | * |
| 183 | */ |
| 184 | private int mSnapIndex; |
| 185 | |
Andy Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 186 | private boolean mSnapEnabled; |
| 187 | |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 188 | /** |
| 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 Sapperstein | 888388c | 2013-08-01 22:51:37 -0700 | [diff] [blame] | 192 | private View mAdditionalBottomBorder; |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 193 | |
| 194 | /** |
| 195 | * A flag denoting whether the fake bottom border has been added to the container. |
| 196 | */ |
Andrew Sapperstein | 888388c | 2013-08-01 22:51:37 -0700 | [diff] [blame] | 197 | private boolean mAdditionalBottomBorderAdded; |
| 198 | |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 199 | /** |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 200 | * 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 Huang | cf5aeae | 2012-03-09 17:25:08 -0800 | [diff] [blame] | 207 | * Child views of this container should implement this interface to be notified when they are |
| 208 | * being detached. |
Andy Huang | cf5aeae | 2012-03-09 17:25:08 -0800 | [diff] [blame] | 209 | */ |
| 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 Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 218 | 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 Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 228 | 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 Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 238 | public ConversationContainer(Context c) { |
| 239 | this(c, null); |
| 240 | } |
| 241 | |
| 242 | public ConversationContainer(Context c, AttributeSet attrs) { |
| 243 | super(c, attrs); |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 244 | |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 245 | mOverlayViews = new SparseArray<OverlayView>(); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 246 | |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 247 | mVelocityTracker = new InputSmoother(c); |
| 248 | |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 249 | 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 Huang | 5ff6374 | 2012-03-16 20:30:23 -0700 | [diff] [blame] | 262 | mWebView = (ConversationWebView) findViewById(R.id.webview); |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 263 | mWebView.addScrollListener(this); |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 264 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 265 | mTopMostOverlay = findViewById(R.id.conversation_topmost_overlay); |
| 266 | |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 267 | 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 Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 273 | } |
| 274 | |
Andrew Sapperstein | 85ea618 | 2013-10-14 18:21:08 -0700 | [diff] [blame] | 275 | public void setupSnapHeader() { |
Andrew Sapperstein | a467d40 | 2013-10-15 18:45:03 -0700 | [diff] [blame] | 276 | mSnapHeader = (SnapHeader) findViewById(R.id.snap_header); |
| 277 | mSnapHeader.setSnappy(); |
Andrew Sapperstein | 85ea618 | 2013-10-14 18:21:08 -0700 | [diff] [blame] | 278 | } |
| 279 | |
Andrew Sapperstein | a467d40 | 2013-10-15 18:45:03 -0700 | [diff] [blame] | 280 | public SnapHeader getSnapHeader() { |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 281 | return mSnapHeader; |
| 282 | } |
| 283 | |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 284 | public void setOverlayAdapter(ConversationViewAdapter a) { |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 285 | if (mOverlayAdapter != null) { |
| 286 | mOverlayAdapter.unregisterDataSetObserver(mAdapterObserver); |
| 287 | clearOverlays(); |
| 288 | } |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 289 | mOverlayAdapter = a; |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 290 | if (mOverlayAdapter != null) { |
| 291 | mOverlayAdapter.registerDataSetObserver(mAdapterObserver); |
| 292 | } |
Andy Huang | 5106713 | 2012-03-12 20:08:19 -0700 | [diff] [blame] | 293 | } |
| 294 | |
| 295 | public Adapter getOverlayAdapter() { |
| 296 | return mOverlayAdapter; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 297 | } |
| 298 | |
Andy Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 299 | public void setAccountController(ConversationAccountController controller) { |
| 300 | mAccountController = controller; |
| 301 | |
| 302 | mSnapEnabled = isSnapEnabled(); |
| 303 | } |
| 304 | |
Andy Huang | 6b3d0d9 | 2012-10-30 15:46:48 -0700 | [diff] [blame] | 305 | /** |
| 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 Huang | 4baafcb | 2012-11-01 18:01:49 -0700 | [diff] [blame] | 316 | // update the snap header too, but only it's showing if the current item |
| 317 | if (i == mSnapIndex && mSnapHeader.isBoundTo(item)) { |
Andy Huang | 6b3d0d9 | 2012-10-30 15:46:48 -0700 | [diff] [blame] | 318 | mSnapHeader.refresh(); |
| 319 | } |
| 320 | } |
| 321 | } |
| 322 | |
Andy Huang | c1fb9a9 | 2013-02-11 13:09:12 -0800 | [diff] [blame] | 323 | /** |
| 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 Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 346 | private void clearOverlays() { |
| 347 | for (int i = 0, len = mOverlayViews.size(); i < len; i++) { |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 348 | detachOverlay(mOverlayViews.valueAt(i)); |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 349 | } |
| 350 | mOverlayViews.clear(); |
| 351 | } |
| 352 | |
| 353 | private void onDataSetChanged() { |
Andy Huang | 2a1e8e3 | 2012-10-23 18:54:57 -0700 | [diff] [blame] | 354 | // 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 Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 359 | clearOverlays(); |
Andy Huang | 2a1e8e3 | 2012-10-23 18:54:57 -0700 | [diff] [blame] | 360 | // 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 Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 363 | |
| 364 | // also clear out the additional bottom border |
| 365 | removeViewInLayout(mAdditionalBottomBorder); |
| 366 | mAdditionalBottomBorderAdded = false; |
| 367 | |
Andy Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 368 | mSnapEnabled = isSnapEnabled(); |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 369 | positionOverlays(0, mOffsetY); |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 370 | } |
| 371 | |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 372 | 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 Huang | 120ea66 | 2012-03-27 23:15:12 -0700 | [diff] [blame] | 386 | |
| 387 | if (!mTouchInitialized) { |
| 388 | mTouchInitialized = true; |
| 389 | } |
| 390 | |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 391 | // no interception when WebView handles the first DOWN |
| 392 | if (mWebView.isHandlingTouch()) { |
| 393 | return false; |
| 394 | } |
| 395 | |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 396 | boolean intercept = false; |
| 397 | switch (ev.getActionMasked()) { |
| 398 | case MotionEvent.ACTION_POINTER_DOWN: |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 399 | LogUtils.d(TAG, "Container is intercepting non-primary touch!"); |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 400 | intercept = true; |
| 401 | mMissedPointerDown = true; |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 402 | requestDisallowInterceptTouchEvent(true); |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 403 | 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 Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 421 | // 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 Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 423 | return intercept; |
| 424 | } |
| 425 | |
| 426 | @Override |
| 427 | public boolean onTouchEvent(MotionEvent ev) { |
| 428 | final int action = ev.getActionMasked(); |
| 429 | |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 430 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 431 | 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 Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 446 | // LogUtils.v(TAG, "in Container.OnTouch. action=%d x/y=%f/%f pointers=%d", |
| 447 | // ev.getActionMasked(), ev.getX(), ev.getY(), ev.getPointerCount()); |
Andy Huang | bb56a15 | 2012-02-24 18:26:47 -0800 | [diff] [blame] | 448 | return webViewResult; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 449 | } |
| 450 | |
| 451 | @Override |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 452 | public void onNotifierScroll(final int x, final int y) { |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 453 | mVelocityTracker.onInput(y); |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 454 | mDisableLayoutTracing = true; |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 455 | positionOverlays(x, y); |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 456 | mDisableLayoutTracing = false; |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 457 | } |
| 458 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 459 | private void positionOverlays(int x, int y) { |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 460 | mOffsetY = y; |
Andy Huang | 07f8732 | 2012-03-27 18:03:53 -0700 | [diff] [blame] | 461 | |
| 462 | /* |
Andy Huang | 120ea66 | 2012-03-27 23:15:12 -0700 | [diff] [blame] | 463 | * 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 Huang | 2301470 | 2012-07-09 12:50:36 -0700 | [diff] [blame] | 465 | * 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 Huang | 07f8732 | 2012-03-27 18:03:53 -0700 | [diff] [blame] | 468 | */ |
Andy Huang | 120ea66 | 2012-03-27 23:15:12 -0700 | [diff] [blame] | 469 | if (mTouchInitialized) { |
Andy Huang | 07f8732 | 2012-03-27 18:03:53 -0700 | [diff] [blame] | 470 | mScale = mWebView.getScale(); |
Andy Huang | 2301470 | 2012-07-09 12:50:36 -0700 | [diff] [blame] | 471 | } else if (mScale == 0) { |
| 472 | mScale = mWebView.getInitialScale(); |
Andy Huang | 07f8732 | 2012-03-27 18:03:53 -0700 | [diff] [blame] | 473 | } |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 474 | traceLayout("in positionOverlays, raw scale=%f, effective scale=%f", mWebView.getScale(), |
| 475 | mScale); |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 476 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 477 | if (mOverlayPositions == null || mOverlayAdapter == null) { |
Andy Huang | 5106713 | 2012-03-12 20:08:19 -0700 | [diff] [blame] | 478 | return; |
| 479 | } |
| 480 | |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 481 | // recycle scrolled-off views and add newly visible views |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 482 | |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 483 | // 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 Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 489 | traceLayout("IN positionOverlays, spacerCount=%d overlayCount=%d", mOverlayPositions.length, |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 490 | mOverlayAdapter.getCount()); |
| 491 | |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 492 | mSnapIndex = -1; |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 493 | mAdditionalBottomBorderOverlayTop = 0; |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 494 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 495 | int adapterLoopIndex = mOverlayAdapter.getCount() - 1; |
| 496 | int spacerIndex = mOverlayPositions.length - 1; |
| 497 | while (spacerIndex >= 0 && adapterLoopIndex >= 0) { |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 498 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 499 | 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 Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 518 | |
| 519 | // always place at least one overlay per spacer |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 520 | ConversationOverlayItem adapterItem = mOverlayAdapter.getItem(adapterIndex); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 521 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 522 | OverlayPosition itemPos = calculatePosition(adapterItem, spacerTop, spacerBottom, |
| 523 | forceGravity); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 524 | |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 525 | traceLayout("in loop, spacer=%d overlay=%d t/b=%d/%d (%s)", spacerIndex, adapterIndex, |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 526 | itemPos.top, itemPos.bottom, adapterItem); |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 527 | positionOverlay(adapterIndex, itemPos.top, itemPos.bottom); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 528 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 529 | // 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 Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 533 | adapterItem = mOverlayAdapter.getItem(adapterIndex); |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 534 | if (spacerIndex > 0 && !adapterItem.isContiguous()) { |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 535 | // advance to the next spacer, but stay on this adapter item |
| 536 | break; |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 537 | } |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 538 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 539 | // 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 Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 544 | |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 545 | traceLayout("in contig loop, spacer=%d overlay=%d t/b=%d/%d (%s)", spacerIndex, |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 546 | adapterIndex, itemPos.top, itemPos.bottom, adapterItem); |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 547 | positionOverlay(adapterIndex, itemPos.top, itemPos.bottom); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 548 | } |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 549 | |
| 550 | spacerIndex--; |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 551 | } |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 552 | |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 553 | positionSnapHeader(mSnapIndex); |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 554 | positionAdditionalBottomBorder(); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 555 | } |
| 556 | |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 557 | /** |
| 558 | * Adds an additional bottom border to the overlay views in case |
| 559 | * the overlays do not fill the entire screen. |
| 560 | */ |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 561 | private void positionAdditionalBottomBorder() { |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 562 | final int lastBottom = mAdditionalBottomBorderOverlayTop; |
Andrew Sapperstein | 83b460b | 2013-08-23 15:33:43 -0700 | [diff] [blame] | 563 | final int containerHeight = webPxToScreenPx(mWebView.getContentHeight()); |
Andrew Sapperstein | 888388c | 2013-08-01 22:51:37 -0700 | [diff] [blame] | 564 | 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 Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 574 | addViewInLayoutWrapper(mAdditionalBottomBorder); |
Andrew Sapperstein | 888388c | 2013-08-01 22:51:37 -0700 | [diff] [blame] | 575 | mAdditionalBottomBorderAdded = true; |
| 576 | } |
| 577 | |
| 578 | measureOverlayView(mAdditionalBottomBorder); |
| 579 | layoutOverlay(mAdditionalBottomBorder, lastBottom, containerHeight); |
| 580 | } else { |
| 581 | if (mAdditionalBottomBorder != null && mAdditionalBottomBorderAdded) { |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 582 | removeViewInLayout(mAdditionalBottomBorder); |
| 583 | mAdditionalBottomBorderAdded = false; |
Andrew Sapperstein | 888388c | 2013-08-01 22:51:37 -0700 | [diff] [blame] | 584 | } |
| 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 Kennedy | ff8553f | 2013-04-05 20:57:44 -0700 | [diff] [blame] | 594 | private static OverlayPosition calculatePosition(final ConversationOverlayItem adapterItem, |
| 595 | final int withinTop, final int withinBottom, final int forceGravity) { |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 596 | 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 Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 615 | /** |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 616 | * 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 Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 629 | * Copied/stolen from {@link ListView}. |
| 630 | */ |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 631 | private void measureOverlayView(View child) { |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 632 | MarginLayoutParams p = (MarginLayoutParams) child.getLayoutParams(); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 633 | if (p == null) { |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 634 | p = (MarginLayoutParams) generateDefaultLayoutParams(); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 635 | } |
| 636 | |
| 637 | int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 638 | getPaddingLeft() + getPaddingRight() + p.leftMargin + p.rightMargin, p.width); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 639 | 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 Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 649 | private void onOverlayScrolledOff(final int adapterIndex, final OverlayView overlay, |
| 650 | int overlayTop, int overlayBottom) { |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 651 | // 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 Huang | 65fe28f | 2012-04-06 18:08:53 -0700 | [diff] [blame] | 655 | mOverlayViews.remove(adapterIndex); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 656 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 657 | post(new Runnable() { |
| 658 | @Override |
| 659 | public void run() { |
| 660 | detachOverlay(overlay); |
| 661 | } |
| 662 | }); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 663 | |
| 664 | // push it out of view immediately |
| 665 | // otherwise this scrolled-off header will continue to draw until the runnable runs |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 666 | layoutOverlay(overlay.view, overlayTop, overlayBottom); |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 667 | } |
| 668 | |
Andy Huang | 8fdce82 | 2012-08-27 15:00:45 -0700 | [diff] [blame] | 669 | /** |
Andy Huang | c69cee3 | 2012-10-07 17:20:39 -0700 | [diff] [blame] | 670 | * Returns an existing scrap view, if available. The view will already be removed from the view |
Andy Huang | 8fdce82 | 2012-08-27 15:00:45 -0700 | [diff] [blame] | 671 | * hierarchy. This method will not remove the view from the scrap heap. |
| 672 | * |
| 673 | */ |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 674 | public View getScrapView(int type) { |
Andy Huang | 8fdce82 | 2012-08-27 15:00:45 -0700 | [diff] [blame] | 675 | return mScrapViews.peek(type); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 676 | } |
| 677 | |
| 678 | public void addScrapView(int type, View v) { |
| 679 | mScrapViews.add(type, v); |
| 680 | } |
| 681 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 682 | private void detachOverlay(OverlayView overlay) { |
Andy Huang | c69cee3 | 2012-10-07 17:20:39 -0700 | [diff] [blame] | 683 | // Prefer removeViewInLayout over removeView. The typical followup layout pass is unneeded |
| 684 | // because removing overlay views doesn't affect overall layout. |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 685 | removeViewInLayout(overlay.view); |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 686 | mScrapViews.add(overlay.itemType, overlay.view); |
| 687 | if (overlay.view instanceof DetachListener) { |
| 688 | ((DetachListener) overlay.view).onDetachedFromParent(); |
Andy Huang | cf5aeae | 2012-03-09 17:25:08 -0800 | [diff] [blame] | 689 | } |
| 690 | } |
| 691 | |
| 692 | @Override |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 693 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 694 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 695 | 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 Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 700 | |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 701 | for (View nonScrollingChild : mNonScrollingChildren) { |
| 702 | if (nonScrollingChild.getVisibility() != GONE) { |
| 703 | measureChildWithMargins(nonScrollingChild, widthMeasureSpec, 0 /* widthUsed */, |
| 704 | heightMeasureSpec, 0 /* heightUsed */); |
| 705 | } |
Andy Huang | 3233bff | 2012-03-20 19:38:45 -0700 | [diff] [blame] | 706 | } |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 707 | mWidthMeasureSpec = widthMeasureSpec; |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 708 | |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 709 | // 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 Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 711 | } |
| 712 | |
| 713 | @Override |
| 714 | protected void onLayout(boolean changed, int l, int t, int r, int b) { |
Andy Huang | 632721e | 2012-04-11 16:57:26 -0700 | [diff] [blame] | 715 | LogUtils.d(TAG, "*** IN header container onLayout"); |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 716 | |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 717 | 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 Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 730 | |
Andy Huang | e20e163 | 2012-06-15 15:59:07 -0700 | [diff] [blame] | 731 | 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 Huang | 8778a87 | 2012-04-20 17:50:41 -0700 | [diff] [blame] | 737 | } |
| 738 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 739 | 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 Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 750 | } |
| 751 | |
| 752 | @Override |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 753 | 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 Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 772 | private int getOverlayTop(int spacerIndex) { |
| 773 | return webPxToScreenPx(mOverlayPositions[spacerIndex].top); |
| 774 | } |
| 775 | |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 776 | private int getOverlayBottom(int spacerIndex) { |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 777 | return webPxToScreenPx(mOverlayPositions[spacerIndex].bottom); |
| 778 | } |
| 779 | |
| 780 | private int webPxToScreenPx(int webPx) { |
Andy Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 781 | // TODO: round or truncate? |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 782 | // TODO: refactor and unify with ConversationWebView.webPxToScreenPx() |
| 783 | return (int) (webPx * mScale); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 784 | } |
| 785 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 786 | private void positionOverlay(int adapterIndex, int overlayTopY, int overlayBottomY) { |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 787 | final OverlayView overlay = mOverlayViews.get(adapterIndex); |
| 788 | final ConversationOverlayItem item = mOverlayAdapter.getItem(adapterIndex); |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 789 | |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 790 | // save off the item's current top for later snap calculations |
| 791 | item.setTop(overlayTopY); |
| 792 | |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 793 | // is the overlay visible and does it have non-zero height? |
| 794 | if (overlayTopY != overlayBottomY && overlayBottomY > mOffsetY |
| 795 | && overlayTopY < mOffsetY + getHeight()) { |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 796 | View overlayView = overlay != null ? overlay.view : null; |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 797 | // show and/or move overlay |
| 798 | if (overlayView == null) { |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 799 | overlayView = addOverlayView(adapterIndex); |
Andrew Sapperstein | 2fd167d | 2014-01-28 10:07:38 -0800 | [diff] [blame] | 800 | ViewCompat.setLayoutDirection(overlayView, ViewCompat.getLayoutDirection(this)); |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 801 | measureOverlayView(overlayView); |
| 802 | item.markMeasurementValid(); |
| 803 | traceLayout("show/measure overlay %d", adapterIndex); |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 804 | } else { |
| 805 | traceLayout("move overlay %d", adapterIndex); |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 806 | if (!item.isMeasurementValid()) { |
Andrew Sapperstein | cee3c90 | 2013-07-31 10:52:02 -0700 | [diff] [blame] | 807 | item.rebindView(overlayView); |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 808 | 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 Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 813 | } |
Andy Huang | 9875bb4 | 2012-04-04 20:36:21 -0700 | [diff] [blame] | 814 | traceLayout("laying out overlay %d with h=%d", adapterIndex, |
| 815 | overlayView.getMeasuredHeight()); |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 816 | final int childBottom = overlayTopY + overlayView.getMeasuredHeight(); |
| 817 | layoutOverlay(overlayView, overlayTopY, childBottom); |
| 818 | mAdditionalBottomBorderOverlayTop = (childBottom > mAdditionalBottomBorderOverlayTop) ? |
| 819 | childBottom : mAdditionalBottomBorderOverlayTop; |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 820 | } else { |
| 821 | // hide overlay |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 822 | if (overlay != null) { |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 823 | traceLayout("hide overlay %d", adapterIndex); |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 824 | onOverlayScrolledOff(adapterIndex, overlay, overlayTopY, overlayBottomY); |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 825 | } else { |
| 826 | traceLayout("ignore non-visible overlay %d", adapterIndex); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 827 | } |
Andrew Sapperstein | ef6367d | 2013-08-04 15:43:46 -0700 | [diff] [blame] | 828 | mAdditionalBottomBorderOverlayTop = (overlayBottomY > mAdditionalBottomBorderOverlayTop) |
| 829 | ? overlayBottomY : mAdditionalBottomBorderOverlayTop; |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 830 | } |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 831 | |
| 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 Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 840 | } |
| 841 | |
| 842 | // layout an existing view |
| 843 | // need its top offset into the conversation, its height, and the scroll offset |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 844 | private void layoutOverlay(View child, int childTop, int childBottom) { |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 845 | final int top = childTop - mOffsetY; |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 846 | final int bottom = childBottom - mOffsetY; |
Andy Huang | 47aa9c9 | 2012-07-31 15:37:21 -0700 | [diff] [blame] | 847 | |
| 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 Huang | b5078b2 | 2012-03-05 19:52:29 -0800 | [diff] [blame] | 852 | } |
| 853 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 854 | private View addOverlayView(int adapterIndex) { |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 855 | final int itemType = mOverlayAdapter.getItemViewType(adapterIndex); |
| 856 | final View convertView = mScrapViews.poll(itemType); |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 857 | |
Andy Huang | 256b35c | 2012-08-22 15:19:13 -0700 | [diff] [blame] | 858 | final View view = mOverlayAdapter.getView(adapterIndex, convertView, this); |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 859 | mOverlayViews.put(adapterIndex, new OverlayView(view, itemType)); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 860 | |
Andy Huang | c69cee3 | 2012-10-07 17:20:39 -0700 | [diff] [blame] | 861 | if (convertView == view) { |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 862 | LogUtils.d(TAG, "want to REUSE scrolled-in view: index=%d obj=%s", adapterIndex, view); |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 863 | } else { |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 864 | LogUtils.d(TAG, "want to CREATE scrolled-in view: index=%d obj=%s", adapterIndex, view); |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 865 | } |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 866 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 867 | addViewInLayoutWrapper(view); |
Andy Huang | 59e0b18 | 2012-08-14 14:32:23 -0700 | [diff] [blame] | 868 | |
Andy Huang | 7bdc375 | 2012-03-25 17:18:19 -0700 | [diff] [blame] | 869 | return view; |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 870 | } |
| 871 | |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 872 | 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 Sapperstein | 888388c | 2013-08-01 22:51:37 -0700 | [diff] [blame] | 876 | } |
| 877 | |
Andy Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 878 | 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 Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 889 | // render and/or re-position snap header |
| 890 | private void positionSnapHeader(int snapIndex) { |
| 891 | ConversationOverlayItem snapItem = null; |
Andy Huang | 8f18778 | 2012-11-06 17:49:25 -0800 | [diff] [blame] | 892 | if (mSnapEnabled && snapIndex != -1) { |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 893 | 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 Huang | 0a648c9 | 2012-10-02 14:56:46 -0700 | [diff] [blame] | 907 | // overlap is negative or zero; bump the snap header upwards by that much |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 908 | 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 Huang | 0a648c9 | 2012-10-02 14:56:46 -0700 | [diff] [blame] | 922 | |
| 923 | mSnapHeader.setTranslationY(overlap); |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 924 | } |
| 925 | |
| 926 | // find the next header that can push the snap header up |
| 927 | private ConversationOverlayItem findNextPushingOverlay(int start) { |
Andy Huang | 31c38a8 | 2012-08-15 21:39:43 -0700 | [diff] [blame] | 928 | 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 Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 937 | /** |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 938 | * 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 Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 951 | * Prevents any layouts from happening until the next time |
| 952 | * {@link #onGeometryChange(OverlayPosition[])} is |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 953 | * called. Useful when you know the HTML spacer coordinates are inconsistent with adapter items. |
| 954 | * <p> |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 955 | * If you call this, you must ensure that a followup call to |
| 956 | * {@link #onGeometryChange(OverlayPosition[])} |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 957 | * is made later, when the HTML spacer coordinates are updated. |
| 958 | * |
| 959 | */ |
| 960 | public void invalidateSpacerGeometry() { |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 961 | mOverlayPositions = null; |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 962 | } |
| 963 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 964 | 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 Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 968 | } |
| 969 | |
Andy Huang | adbf3e8 | 2012-10-13 13:30:19 -0700 | [diff] [blame] | 970 | mOverlayPositions = overlayPositions; |
Andrew Sapperstein | d97eb87 | 2014-04-16 16:48:03 -0700 | [diff] [blame] | 971 | positionOverlays(0, mOffsetY); |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 972 | } |
| 973 | |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 974 | private void traceLayout(String msg, Object... params) { |
| 975 | if (mDisableLayoutTracing) { |
| 976 | return; |
| 977 | } |
Andy Huang | 7f9ef60 | 2012-07-25 16:44:30 -0700 | [diff] [blame] | 978 | LogUtils.d(TAG, msg, params); |
Andy Huang | c754357 | 2012-04-03 15:34:29 -0700 | [diff] [blame] | 979 | } |
| 980 | |
Andy Huang | 46dfba6 | 2012-04-19 01:47:32 -0700 | [diff] [blame] | 981 | private class AdapterObserver extends DataSetObserver { |
| 982 | @Override |
| 983 | public void onChanged() { |
| 984 | onDataSetChanged(); |
| 985 | } |
| 986 | } |
Andy Huang | f70fc40 | 2012-02-17 15:37:42 -0800 | [diff] [blame] | 987 | } |