1/*
2 * Copyright (C) 2010 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 */
16
17package com.android.camera.ui;
18
19import static android.view.View.MeasureSpec.makeMeasureSpec;
20
21import android.content.Context;
22import android.graphics.Rect;
23import android.os.Handler;
24import android.os.Message;
25import android.view.GestureDetector;
26import android.view.MotionEvent;
27import android.view.View.MeasureSpec;
28import android.view.animation.AlphaAnimation;
29import android.view.animation.Animation;
30import android.view.animation.Transformation;
31import android.widget.Scroller;
32
33import com.android.camera.Util;
34
35import javax.microedition.khronos.opengles.GL11;
36
37public class GLListView extends GLView {
38    @SuppressWarnings("unused")
39    private static final String TAG = "GLListView";
40    private static final int INDEX_NONE = -1;
41    private static final int SCROLL_BAR_TIMEOUT = 2500;
42
43    private static final int HIDE_SCROLL_BAR = 1;
44
45    private Model mModel;
46    private Handler mHandler;
47
48    private int mHighlightIndex = INDEX_NONE;
49    private GLView mHighlightView;
50
51    private NinePatchTexture mHighLight;
52    private NinePatchTexture mScrollbar;
53
54    private int mVisibleStart = 0; // inclusive
55    private int mVisibleEnd = 0; // exclusive
56
57    private boolean mHasMeasured = false;
58
59    private boolean mScrollBarVisible = false;
60    private Animation mScrollBarAnimation;
61    private OnItemSelectedListener mOnItemSelectedListener;
62
63    private GestureDetector mGestureDetector;
64    private final Scroller mScroller;
65    private boolean mScrollable;
66    private boolean mIsPressed = false;
67
68    static public interface Model {
69        public int size();
70        public GLView getView(int index);
71        public boolean isSelectable(int index);
72    }
73
74    static public interface OnItemSelectedListener {
75        public void onItemSelected(GLView view, int position);
76    }
77
78    public GLListView(Context context) {
79        mScroller = new Scroller(context);
80    }
81
82    private final Runnable mHideScrollBar = new Runnable() {
83        public void run() {
84            setScrollBarVisible(false);
85        }
86    };
87
88    @Override
89    protected void onVisibilityChanged(int visibility) {
90        super.onVisibilityChanged(visibility);
91        if (visibility == GLView.VISIBLE && mScrollHeight > getHeight()) {
92            setScrollBarVisible(true);
93            mHandler.sendEmptyMessageDelayed(
94                    HIDE_SCROLL_BAR, SCROLL_BAR_TIMEOUT);
95        }
96    }
97
98    @Override
99    protected void onAttachToRoot(GLRootView root) {
100        super.onAttachToRoot(root);
101        mHandler = new Handler(root.getTimerLooper()) {
102            @Override
103            public void handleMessage(Message msg) {
104                GLRootView root = getGLRootView();
105                switch(msg.what) {
106                    case HIDE_SCROLL_BAR:
107                        root.queueEvent(mHideScrollBar);
108                        break;
109                }
110            }
111        };
112        mGestureDetector =
113            new GestureDetector(root.getContext(),
114            new MyGestureListener(), mHandler);
115    }
116
117
118    private void setScrollBarVisible(boolean visible) {
119        if (mScrollBarVisible == visible || mScrollbar == null) return;
120        mScrollBarVisible = visible;
121        if (!visible) {
122            mScrollBarAnimation = new AlphaAnimation(1, 0);
123            mScrollBarAnimation.setDuration(300);
124            mScrollBarAnimation.start();
125        } else {
126            mScrollBarAnimation = null;
127        }
128        invalidate();
129    }
130
131    public void setHighLight(NinePatchTexture highLight) {
132        mHighLight = highLight;
133    }
134
135    public void setDataModel(Model model) {
136        mModel = model;
137        mScrollY = 0;
138        requestLayout();
139    }
140
141    public void setOnItemSelectedListener(OnItemSelectedListener l) {
142        mOnItemSelectedListener = l;
143    }
144
145    private boolean drawWithAnimation(GLRootView root,
146            Texture texture, int x, int y, Animation anim) {
147        long now = root.currentAnimationTimeMillis();
148        Transformation temp = root.obtainTransformation();
149        boolean more = anim.getTransformation(now, temp);
150        Transformation transformation = root.pushTransform();
151        transformation.compose(temp);
152        texture.draw(root, x, y);
153        invalidate();
154        root.popTransform();
155        return more;
156    }
157
158    @Override
159    protected void render(GLRootView root, GL11 gl) {
160        root.clipRect(0, 0, getWidth(), getHeight());
161        if (mHighlightIndex != INDEX_NONE) {
162            GLView view = mModel.getView(mHighlightIndex);
163            Rect bounds = view.bounds();
164            if (mHighLight != null) {
165                int width = bounds.width();
166                int height = bounds.height();
167                mHighLight.setSize(width, height);
168                mHighLight.draw(root,
169                        bounds.left - mScrollX, bounds.top - mScrollY);
170            }
171        }
172        super.render(root, gl);
173        root.clearClip();
174
175        if (mScrollBarAnimation != null || mScrollBarVisible) {
176            int width = mScrollbar.getIntrinsicWidth();
177            int height = getHeight() * getHeight() / mScrollHeight;
178            int yoffset = mScrollY * getHeight() / mScrollHeight;
179            mScrollbar.setSize(width, height);
180            if (mScrollBarAnimation != null) {
181                if (!drawWithAnimation(root, mScrollbar,
182                        getWidth() - width, yoffset, mScrollBarAnimation)) {
183                    mScrollBarAnimation = null;
184                }
185            } else {
186                mScrollbar.draw(root, getWidth() - width, yoffset);
187            }
188        }
189        if (mScroller.computeScrollOffset()) {
190            setScrollPosition(mScroller.getCurrY(), false);
191        }
192    }
193
194    @Override
195    protected void onMeasure(int widthSpec, int heightSpec) {
196        // first get the total height
197        int height = 0;
198        int maxWidth = 0;
199        for (int i = 0, n = mModel.size(); i < n; ++i) {
200            GLView view = mModel.getView(i);
201            view.measure(widthSpec, MeasureSpec.UNSPECIFIED);
202            height += view.getMeasuredHeight();
203            maxWidth = Math.max(maxWidth, view.getMeasuredWidth());
204        }
205        mScrollHeight = height;
206        mHasMeasured = true;
207        new MeasureHelper(this)
208                .setPreferredContentSize(maxWidth, height)
209                .measure(widthSpec, heightSpec);
210    }
211
212    @Override
213    public int getComponentCount() {
214        return mVisibleEnd - mVisibleStart;
215    }
216
217    @Override
218    public GLView getComponent(int index) {
219        if (index < 0 || index >= mVisibleEnd - mVisibleStart) {
220            throw new ArrayIndexOutOfBoundsException(index);
221        }
222        return mModel.getView(mVisibleStart + index);
223    }
224
225    @Override
226    public void requestLayout() {
227        mHasMeasured = false;
228        super.requestLayout();
229    }
230
231    @Override
232    protected void onLayout(
233            boolean change, int left, int top, int right, int bottom) {
234
235        if (!mHasMeasured || mMeasuredWidth != (right - left)) {
236            measure(makeMeasureSpec(right - left, MeasureSpec.EXACTLY),
237                    makeMeasureSpec(bottom - top, MeasureSpec.EXACTLY));
238        }
239
240        mScrollable = mScrollHeight > (bottom - top);
241        int width = right - left;
242        int yoffset = 0;
243
244        for (int i = 0, n = mModel.size(); i < n; ++i) {
245            GLView item = mModel.getView(i);
246            item.onAddToParent(this);
247            int nextOffset = yoffset + item.getMeasuredHeight();
248            item.layout(0, yoffset, width, nextOffset);
249            yoffset = nextOffset;
250        }
251        setScrollPosition(mScrollY, true);
252    }
253
254    private void setScrollPosition(int position, boolean force) {
255        int height = getHeight();
256
257        position = Util.clamp(position, 0, mScrollHeight - height);
258
259        if (!force && position == mScrollY) return;
260        mScrollY = position;
261
262        int n = mModel.size();
263
264        int start = 0;
265        int end = 0;
266        for (start = 0; start < n; ++start) {
267            if (position < mModel.getView(start).mBounds.bottom) break;
268        }
269
270        int bottom = position + height;
271        for (end = start; end < n; ++ end) {
272            if (bottom <= mModel.getView(end).mBounds.top) break;
273        }
274        setVisibleRange(start , end);
275        invalidate();
276    }
277
278    private void setVisibleRange(int start, int end) {
279        if (start == mVisibleStart && end == mVisibleEnd) return;
280        mVisibleStart = start;
281        mVisibleEnd = end;
282    }
283
284    @Override
285    protected boolean dispatchTouchEvent(MotionEvent event) {
286        return onTouch(event);
287    }
288
289    @Override @SuppressWarnings("fallthrough")
290    protected boolean onTouch(MotionEvent event) {
291
292        mGestureDetector.onTouchEvent(event);
293
294        switch (event.getAction()) {
295            case MotionEvent.ACTION_DOWN:
296                mHandler.removeMessages(HIDE_SCROLL_BAR);
297                setScrollBarVisible(mScrollHeight > getHeight());
298                break;
299            case MotionEvent.ACTION_MOVE:
300                mIsPressed = true;
301                if (!mScrollable) {
302                    findAndSetHighlightItem((int) event.getY());
303                }
304                break;
305            case MotionEvent.ACTION_UP:
306                mIsPressed = false;
307                if (mScrollBarVisible) {
308                    mHandler.removeMessages(HIDE_SCROLL_BAR);
309                    mHandler.sendEmptyMessageDelayed(
310                            HIDE_SCROLL_BAR, SCROLL_BAR_TIMEOUT);
311                }
312                if (!mScrollable && mOnItemSelectedListener != null
313                        && mHighlightView != null) {
314                    mOnItemSelectedListener
315                            .onItemSelected(mHighlightView, mHighlightIndex);
316                }
317            case MotionEvent.ACTION_CANCEL:
318            case MotionEvent.ACTION_OUTSIDE:
319                setHighlightItem(null, INDEX_NONE);
320        }
321        return true;
322    }
323
324    private void findAndSetHighlightItem(int y) {
325        int position = y + mScrollY;
326        for (int i = mVisibleStart, n = mVisibleEnd; i < n; ++i) {
327            GLView child = mModel.getView(i);
328            if (child.mBounds.bottom > position) {
329                if (mModel.isSelectable(i)) {
330                    setHighlightItem(child, i);
331                    return;
332                }
333                break;
334            }
335        }
336        setHighlightItem(null, INDEX_NONE);
337    }
338
339    private void setHighlightItem(GLView view, int index) {
340        if (index == mHighlightIndex) return;
341        mHighlightIndex = index;
342        mHighlightView = view;
343        if (mHighLight != null) invalidate();
344    }
345
346    public void setScroller(NinePatchTexture scrollbar) {
347        this.mScrollbar = scrollbar;
348        requestLayout();
349    }
350
351    private class MyGestureListener
352            extends GestureDetector.SimpleOnGestureListener {
353
354        @Override
355        public boolean onFling(MotionEvent e1,
356                MotionEvent e2, float velocityX, float velocityY) {
357            if (!mScrollable) return false;
358            mScroller.fling(0, mScrollY,
359                    0, -(int) velocityY, 0, 0, 0, mScrollHeight - getHeight());
360            invalidate();
361            return true;
362        }
363
364        @Override
365        public boolean onScroll(MotionEvent e1,
366                MotionEvent e2, float distanceX, float distanceY) {
367            if (!mScrollable) return false;
368            setHighlightItem(null, INDEX_NONE);
369            setScrollPosition(mScrollY + (int) distanceY, false);
370            return true;
371        }
372
373        @Override
374        public void onShowPress(MotionEvent e) {
375            if (!mScrollable) return;
376            final int y = (int) e.getY();
377            getGLRootView().queueEvent(new Runnable() {
378                public void run() {
379                    if (mIsPressed) findAndSetHighlightItem(y);
380                }
381            });
382        }
383
384        @Override
385        public boolean onSingleTapUp(MotionEvent e) {
386            if (!mScrollable) return false;
387            findAndSetHighlightItem((int) e.getY());
388            if (mOnItemSelectedListener != null && mHighlightView != null) {
389                mOnItemSelectedListener
390                        .onItemSelected(mHighlightView, mHighlightIndex);
391            }
392            setHighlightItem(null, INDEX_NONE);
393            return true;
394        }
395    }
396}
397