PhotoTouchListener.java revision c1501041b64faa6c205a93baf403c4c87a0c1acf
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.dreams.phototable;
17
18import android.content.Context;
19import android.content.res.Resources;
20import android.util.Log;
21import android.view.MotionEvent;
22import android.view.View;
23import android.view.ViewConfiguration;
24import android.view.ViewPropertyAnimator;
25import android.view.animation.DecelerateInterpolator;
26
27/**
28 * Touch listener that implements phototable interactions.
29 */
30public class PhotoTouchListener implements View.OnTouchListener {
31    private static final String TAG = "PhotoTouchListener";
32    private static final boolean DEBUG = false;
33    private static final int INVALID_POINTER = -1;
34    private static final int MAX_POINTER_COUNT = 10;
35    private final int mTouchSlop;
36    private final int mTapTimeout;
37    private final Table mTable;
38    private final float mBeta;
39    private final float mTableRatio;
40    private final boolean mEnableFling;
41    private final boolean mManualImageRotation;
42    private long mLastEventTime;
43    private float mLastTouchX;
44    private float mLastTouchY;
45    private float mInitialTouchX;
46    private float mInitialTouchY;
47    private float mInitialTouchA;
48    private long mInitialTouchTime;
49    private float mInitialTargetX;
50    private float mInitialTargetY;
51    private float mInitialTargetA;
52    private float mDX;
53    private float mDY;
54    private int mA = INVALID_POINTER;
55    private int mB = INVALID_POINTER;
56    private float[] pts = new float[MAX_POINTER_COUNT];
57    private float[] tmp = new float[MAX_POINTER_COUNT];
58
59    public PhotoTouchListener(Context context, Table table) {
60        mTable = table;
61        final ViewConfiguration configuration = ViewConfiguration.get(context);
62        mTouchSlop = configuration.getScaledTouchSlop();
63        mTapTimeout = configuration.getTapTimeout();
64        final Resources resources = context.getResources();
65        mBeta = resources.getInteger(R.integer.table_damping) / 1000000f;
66        mTableRatio = resources.getInteger(R.integer.table_ratio) / 1000000f;
67        mEnableFling = resources.getBoolean(R.bool.enable_fling);
68        mManualImageRotation = resources.getBoolean(R.bool.enable_manual_image_rotation);
69    }
70
71    /** Get angle defined by first two touches, in degrees */
72    private float getAngle(View target, MotionEvent ev) {
73        float alpha = 0f;
74        int a = ev.findPointerIndex(mA);
75        int b = ev.findPointerIndex(mB);
76        if (a >=0 && b >=0) {
77            alpha = (float) (Math.atan2(pts[2*a + 1] - pts[2*b + 1],
78                                        pts[2*a] - pts[2*b]) *
79                             180f / Math.PI);
80        }
81        return alpha;
82    }
83
84    private void resetTouch(View target) {
85        mInitialTouchX = -1;
86        mInitialTouchY = -1;
87        mInitialTouchA = 0f;
88        mInitialTargetX = (float) target.getX();
89        mInitialTargetY = (float) target.getY();
90        mInitialTargetA = (float) target.getRotation();
91    }
92
93    public void onFling(View target, float dX, float dY) {
94        if (!mEnableFling) {
95            return;
96        }
97        log("fling " + dX + ", " + dY);
98
99        // convert to pixel per frame
100        dX /= 60f;
101        dY /= 60f;
102
103        // starting position compionents in global corrdinate frame
104        final float x0 = pts[0];
105        final float y0 = pts[1];
106
107        // velocity
108        final float v = (float) Math.hypot(dX, dY);
109
110        if (v == 0f) {
111            return;
112        }
113
114        // number of steps to come to a stop
115        final float n = (float) Math.max(1.0, (- Math.log(v) / Math.log(mBeta)));
116        // distance travelled before stopping
117        final float s = (float) Math.max(0.0, (v * (1f - Math.pow(mBeta, n)) / (1f - mBeta)));
118
119        // ending posiiton after stopping
120        final float x1 = x0 + s * dX / v;
121        final float y1 = y0 + s * dY / v;
122
123        final float photoWidth = ((Integer) target.getTag(R.id.photo_width)).floatValue();
124        final float photoHeight = ((Integer) target.getTag(R.id.photo_height)).floatValue();
125        final float tableWidth = mTable.getWidth();
126        final float tableHeight = mTable.getHeight();
127        final float halfShortSide =
128                Math.min(photoWidth * mTableRatio, photoHeight * mTableRatio) / 2f;
129        final View photo = target;
130        ViewPropertyAnimator animator = photo.animate()
131                .withLayer()
132                .xBy(x1 - x0)
133                .yBy(y1 - y0)
134                .setDuration((int) (1000f * n / 60f))
135                .setInterpolator(new DecelerateInterpolator(2f));
136
137        if (y1 + halfShortSide < 0f || y1 - halfShortSide > tableHeight ||
138            x1 + halfShortSide < 0f || x1 - halfShortSide > tableWidth) {
139            log("fling away");
140            animator.withEndAction(new Runnable() {
141                    @Override
142                    public void run() {
143                        mTable.fadeAway(photo, true);
144                    }
145                });
146        }
147    }
148
149    @Override
150    public boolean onTouch(View target, MotionEvent ev) {
151        final int action = ev.getActionMasked();
152
153        // compute raw coordinates
154        for(int i = 0; i < 10 && i < ev.getPointerCount(); i++) {
155            pts[i*2] = ev.getX(i);
156            pts[i*2 + 1] = ev.getY(i);
157        }
158        target.getMatrix().mapPoints(pts);
159
160        switch (action) {
161        case MotionEvent.ACTION_DOWN:
162            mTable.moveToBackOfQueue(target);
163            mInitialTouchTime = ev.getEventTime();
164            mA = ev.getPointerId(ev.getActionIndex());
165            resetTouch(target);
166            break;
167
168        case MotionEvent.ACTION_POINTER_DOWN:
169            if (mB == INVALID_POINTER) {
170                mB = ev.getPointerId(ev.getActionIndex());
171                mInitialTouchA = getAngle(target, ev);
172            }
173            break;
174
175        case MotionEvent.ACTION_POINTER_UP:
176            if (mB == ev.getPointerId(ev.getActionIndex())) {
177                mB = INVALID_POINTER;
178                mInitialTargetA = (float) target.getRotation();
179            }
180            if (mA == ev.getPointerId(ev.getActionIndex())) {
181                log("primary went up!");
182                mA = mB;
183                resetTouch(target);
184                mB = INVALID_POINTER;
185            }
186            break;
187
188        case MotionEvent.ACTION_MOVE: {
189                if (mA != INVALID_POINTER) {
190                    int idx = ev.findPointerIndex(mA);
191                    float x = pts[2 * idx];
192                    float y = pts[2 * idx + 1];
193                    if (mInitialTouchX == -1 && mInitialTouchY == -1) {
194                        mInitialTouchX = x;
195                        mInitialTouchY = y;
196                    } else {
197                        float dt = (float) (ev.getEventTime() - mLastEventTime) / 1000f;
198                        float tmpDX = (x - mLastTouchX) / dt;
199                        float tmpDY = (y - mLastTouchY) / dt;
200                        if (dt > 0f && (Math.abs(tmpDX) > 5f || Math.abs(tmpDY) > 5f)) {
201                            // work around odd bug with multi-finger flings
202                            mDX = tmpDX;
203                            mDY = tmpDY;
204                        }
205                        log("move " + mDX + ", " + mDY);
206
207                        mLastEventTime = ev.getEventTime();
208                        mLastTouchX = x;
209                        mLastTouchY = y;
210                    }
211
212                    if (mTable.getSelected() != target) {
213                        target.animate().cancel();
214
215                        target.setX((int) (mInitialTargetX + x - mInitialTouchX));
216                        target.setY((int) (mInitialTargetY + y - mInitialTouchY));
217                        if (mManualImageRotation && mB != INVALID_POINTER) {
218                            float a = getAngle(target, ev);
219                            target.setRotation(
220                                    (int) (mInitialTargetA + a - mInitialTouchA));
221                        }
222                    }
223                }
224            }
225            break;
226
227        case MotionEvent.ACTION_UP: {
228                if (mA != INVALID_POINTER) {
229                    int idx = ev.findPointerIndex(mA);
230                    float x0 = pts[2 * idx];
231                    float y0 = pts[2 * idx + 1];
232                    if (mInitialTouchX == -1 && mInitialTouchY == -1) {
233                        mInitialTouchX = x0;
234                        mInitialTouchY = y0;
235                    }
236                    double distance = Math.hypot(x0 - mInitialTouchX,
237                                                 y0 - mInitialTouchY);
238                    if (mTable.getSelected() == target) {
239                        mTable.dropOnTable(target);
240                        mTable.clearSelection();
241                    } else if ((ev.getEventTime() - mInitialTouchTime) < mTapTimeout &&
242                               distance < mTouchSlop) {
243                        // tap
244                        target.animate().cancel();
245                        mTable.setSelection(target);
246                    } else {
247                        onFling(target, mDX, mDY);
248                    }
249                    mA = INVALID_POINTER;
250                    mB = INVALID_POINTER;
251                }
252            }
253            break;
254
255        case MotionEvent.ACTION_CANCEL:
256            log("action cancel!");
257            break;
258        }
259
260        return true;
261    }
262
263    private static void log(String message) {
264        if (DEBUG) {
265            Log.i(TAG, message);
266        }
267    }
268}
269