1 | // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | package org.chromium.content.browser; |
6 | |
7 | import android.content.Context; |
8 | import android.util.DisplayMetrics; |
9 | import android.util.Log; |
10 | import android.view.MotionEvent; |
11 | |
12 | /** |
13 | * This objects controls the scroll snapping behavior based on scroll updates. |
14 | */ |
15 | class SnapScrollController { |
16 | private static final String TAG = "SnapScrollController"; |
17 | private static final int SNAP_NONE = 0; |
18 | private static final int SNAP_HORIZ = 1; |
19 | private static final int SNAP_VERT = 2; |
20 | private static final int SNAP_BOUND = 16; |
21 | |
22 | private float mChannelDistance = 16f; |
23 | private int mSnapScrollMode = SNAP_NONE; |
24 | private int mFirstTouchX = -1; |
25 | private int mFirstTouchY = -1; |
26 | private float mDistanceX = 0; |
27 | private float mDistanceY = 0; |
28 | private ZoomManager mZoomManager; |
29 | |
30 | SnapScrollController(Context context, ZoomManager zoomManager) { |
31 | calculateChannelDistance(context); |
32 | mZoomManager = zoomManager; |
33 | } |
34 | |
35 | /** |
36 | * Updates the snap scroll mode based on the given X and Y distance to be moved on scroll. |
37 | * If the scroll update is above a threshold, the snapping behavior is reset. |
38 | * @param distanceX X distance for the current scroll update. |
39 | * @param distanceY Y distance for the current scroll update. |
40 | */ |
41 | void updateSnapScrollMode(float distanceX, float distanceY) { |
42 | if (mSnapScrollMode == SNAP_HORIZ || mSnapScrollMode == SNAP_VERT) { |
43 | mDistanceX += Math.abs(distanceX); |
44 | mDistanceY += Math.abs(distanceY); |
45 | if (mSnapScrollMode == SNAP_HORIZ) { |
46 | if (mDistanceY > mChannelDistance) { |
47 | mSnapScrollMode = SNAP_NONE; |
48 | } else if (mDistanceX > mChannelDistance) { |
49 | mDistanceX = 0; |
50 | mDistanceY = 0; |
51 | } |
52 | } else { |
53 | if (mDistanceX > mChannelDistance) { |
54 | mSnapScrollMode = SNAP_NONE; |
55 | } else if (mDistanceY > mChannelDistance) { |
56 | mDistanceX = 0; |
57 | mDistanceY = 0; |
58 | } |
59 | } |
60 | } |
61 | } |
62 | |
63 | /** |
64 | * Sets the snap scroll mode based on the event type. |
65 | * @param event The received MotionEvent. |
66 | */ |
67 | void setSnapScrollingMode(MotionEvent event) { |
68 | switch(event.getAction()) { |
69 | case MotionEvent.ACTION_DOWN: |
70 | mSnapScrollMode = SNAP_NONE; |
71 | mFirstTouchX = (int) event.getX(); |
72 | mFirstTouchY = (int) event.getY(); |
73 | break; |
74 | // Set scrolling mode to SNAP_X if scroll towards x-axis exceeds SNAP_BOUND |
75 | // and movement towards y-axis is trivial. |
76 | // Set scrolling mode to SNAP_Y if scroll towards y-axis exceeds SNAP_BOUND |
77 | // and movement towards x-axis is trivial. |
78 | // Scrolling mode will remain in SNAP_NONE for other conditions. |
79 | case MotionEvent.ACTION_MOVE: |
80 | if (!mZoomManager.isScaleGestureDetectionInProgress() && |
81 | mSnapScrollMode == SNAP_NONE) { |
82 | int xDiff = (int) Math.abs(event.getX() - mFirstTouchX); |
83 | int yDiff = (int) Math.abs(event.getY() - mFirstTouchY); |
84 | if (xDiff > SNAP_BOUND && yDiff < SNAP_BOUND) { |
85 | mSnapScrollMode = SNAP_HORIZ; |
86 | } else if (xDiff < SNAP_BOUND && yDiff > SNAP_BOUND) { |
87 | mSnapScrollMode = SNAP_VERT; |
88 | } |
89 | } |
90 | break; |
91 | case MotionEvent.ACTION_UP: |
92 | case MotionEvent.ACTION_CANCEL: |
93 | mFirstTouchX = -1; |
94 | mFirstTouchY = -1; |
95 | mDistanceX = 0; |
96 | mDistanceY = 0; |
97 | break; |
98 | default: |
99 | Log.i(TAG, "setSnapScrollingMode case-default no-op"); |
100 | break; |
101 | } |
102 | } |
103 | |
104 | private void calculateChannelDistance(Context context) { |
105 | // The channel distance is adjusted for density and screen size. |
106 | final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); |
107 | final double screenSize = Math.hypot((double) metrics.widthPixels / metrics.densityDpi, |
108 | (double) metrics.heightPixels / metrics.densityDpi); |
109 | if (screenSize < 3.0) { |
110 | mChannelDistance = 16f; |
111 | } else if (screenSize < 5.0) { |
112 | mChannelDistance = 22f; |
113 | } else if (screenSize < 7.0) { |
114 | mChannelDistance = 28f; |
115 | } else { |
116 | mChannelDistance = 34f; |
117 | } |
118 | mChannelDistance = mChannelDistance * metrics.density; |
119 | if (mChannelDistance < 16f) mChannelDistance = 16f; |
120 | } |
121 | |
122 | /** |
123 | * Resets the snap scroll mode to default. |
124 | */ |
125 | void resetSnapScrollMode() { |
126 | mSnapScrollMode = SNAP_NONE; |
127 | } |
128 | |
129 | /** |
130 | * @return Whether current snap scroll mode is vertical. |
131 | */ |
132 | boolean isSnapVertical() { |
133 | return mSnapScrollMode == SNAP_VERT; |
134 | } |
135 | |
136 | /** |
137 | * @return Whether current snap scroll mode is horizontal. |
138 | */ |
139 | boolean isSnapHorizontal() { |
140 | return mSnapScrollMode == SNAP_HORIZ; |
141 | } |
142 | |
143 | /** |
144 | * @return Whether currently snapping scrolls. |
145 | */ |
146 | boolean isSnappingScrolls() { |
147 | return mSnapScrollMode != SNAP_NONE; |
148 | } |
149 | } |