1 package com.cooliris.media;
3 import java.util.ArrayList;
4 import java.util.Calendar;
5 import java.util.GregorianCalendar;
6 import java.util.HashMap;
8 import javax.microedition.khronos.opengles.GL11;
10 import android.content.Context;
11 import android.graphics.Bitmap;
12 import android.graphics.BitmapFactory;
13 import android.graphics.Canvas;
14 import android.graphics.NinePatch;
15 import android.graphics.Paint;
16 import android.graphics.PorterDuff;
17 import android.graphics.PorterDuffXfermode;
18 import android.graphics.Rect;
19 import android.util.SparseArray;
20 import android.view.MotionEvent;
21 import com.cooliris.media.RenderView.Lists;
23 public final class TimeBar extends Layer implements MediaFeed.Listener {
24 public static final int HEIGHT = 48;
25 private static final int MARKER_SPACING_PIXELS = 50;
26 private static final float AUTO_SCROLL_MARGIN = 100f;
27 private static final Paint SRC_PAINT = new Paint();
28 private final Context mContext;
29 private Listener mListener = null;
30 private MediaFeed mFeed = null;
31 private float mTotalWidth = 0f;
32 private float mPosition = 0f;
33 private float mPositionAnim = 0f;
34 private float mScroll = 0f;
35 private float mScrollAnim = 0f;
36 private boolean mInDrag = false;
37 private float mDragX = 0f;
39 private ArrayList<Marker> mMarkers = new ArrayList<Marker>();
40 private ArrayList<Marker> mMarkersCopy = new ArrayList<Marker>();
42 private static final int KNOB = R.drawable.scroller_new;
43 private static final int KNOB_PRESSED = R.drawable.scroller_pressed_new;
44 private final StringTexture.Config mMonthYearFormat = new StringTexture.Config();
45 private final StringTexture.Config mDayFormat = new StringTexture.Config();
46 private final SparseArray<StringTexture> mYearLabels = new SparseArray<StringTexture>();
47 private final StringTexture mDateUnknown;
48 private final StringTexture[] mMonthLabels = new StringTexture[12];
49 private final StringTexture[] mDayLabels = new StringTexture[32];
50 private final StringTexture[] mOpaqueDayLabels = new StringTexture[32];
51 private final StringTexture mDot = new StringTexture("¥");
52 private final HashMap<MediaItem, Marker> mTracker = new HashMap<MediaItem, Marker>(1024);
54 private float mTextAlpha = 0.0f;
55 private float mAnimTextAlpha = 0.0f;
56 private boolean mShowTime;
57 private NinePatch mBackground;
58 private Rect mBackgroundRect;
59 private BitmapTexture mBackgroundTexture;
61 public interface Listener {
62 public void onTimeChanged(TimeBar timebar);
65 TimeBar(Context context) {
69 // Setup formatting for text labels.
70 mMonthYearFormat.fontSize = 17f * Gallery.PIXEL_DENSITY;
71 mMonthYearFormat.bold = true;
72 mMonthYearFormat.a = 0.85f;
73 mDayFormat.fontSize = 17f * Gallery.PIXEL_DENSITY;
76 // Create textures for month names.
77 String[] months = mContext.getResources().getStringArray(R.array.months_abbreviated);
78 for (int i = 0; i < months.length; ++i) {
79 mMonthLabels[i] = new StringTexture(months[i], mMonthYearFormat);
82 for (int i = 0; i <= 31; ++i) {
83 mDayLabels[i] = new StringTexture(Integer.toString(i), mDayFormat);
84 mOpaqueDayLabels[i] = new StringTexture(Integer.toString(i), mMonthYearFormat);
86 mDateUnknown = new StringTexture(context.getResources().getString(R.string.date_unknown), mMonthYearFormat);
87 Bitmap background = BitmapFactory.decodeResource(context.getResources(), R.drawable.popup);
88 mBackground = new NinePatch(background, background.getNinePatchChunk(), null);
89 mBackgroundRect = new Rect();
90 SRC_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
93 public void setListener(Listener listener) {
97 public void setFeed(MediaFeed feed, int state, boolean needsLayout) {
103 mScroll = getScrollForPosition(mPosition);
108 protected void onSizeChanged() {
109 mScroll = getScrollForPosition(mPosition);
112 // public long getTime() {
113 // final float x = mPosition * mTotalWidth;
114 // for (int i = 0, size = mMarkers.size(); i < size; ++i) {
115 // Marker marker = mMarkers.get(i);
116 // if (x <= marker.x) {
117 // return marker.time;
123 // public void setTime(long time) {
124 // synchronized (mMarkers) {
125 // for (int i = 0, size = mMarkers.size(); i < size; ++i) {
126 // Marker marker = mMarkers.get(i);
127 // if (time <= marker.time) {
128 // mPosition = Math.max(0.0f, Math.min(1.0f, marker.x / mTotalWidth));
129 // mScroll = getScrollForPosition(mPosition);
136 public MediaItem getItem() {
137 synchronized (mMarkers) {
138 // x is between 0 and 1.0f
139 int numMarkers = mMarkers.size();
142 int index = (int) (mPosition * (numMarkers));
143 if (index >= numMarkers)
144 index = numMarkers - 1;
145 Marker marker = mMarkers.get(index);
146 if (marker != null) {
147 // we have to find the index of the media item depending upon
148 // the value of mPosition
149 float deltaBetweenMarkers = 1.0f / numMarkers;
150 float increment = mPosition - index * deltaBetweenMarkers;
151 // if (increment > deltaBetweenMarkers)
152 // increment = deltaBetweenMarkers;
153 // if (increment < 0)
155 ArrayList<MediaItem> items = marker.items;
156 int numItems = items.size();
159 int itemIndex = (int) ((numItems) * increment / deltaBetweenMarkers);
160 if (itemIndex >= numItems)
161 itemIndex = numItems - 1;
162 return marker.items.get(itemIndex);
168 private Marker getAnchorMarker() {
169 synchronized (mMarkers) {
170 // x is between 0 and 1.0f
171 int numMarkers = mMarkers.size();
174 int index = (int) (mPosition * (numMarkers));
175 if (index >= numMarkers)
176 index = numMarkers - 1;
177 Marker marker = mMarkers.get(index);
182 public void setItem(MediaItem item) {
183 Marker marker = mTracker.get(item);
184 if (marker != null) {
185 float markerX = (mTotalWidth == 0.0f) ? 0.0f : marker.x / mTotalWidth;
186 mPosition = Math.max(0.0f, Math.min(1.0f, markerX));
187 mScroll = getScrollForPosition(mPosition);
191 private void layout() {
193 // Clear existing markers.
195 synchronized (mMarkers) {
198 float scrollX = mScroll;
199 // Place markers for every time interval that intersects one of the
201 // Markers for a full month would be for example: Jan 5 10 15 20 25
203 MediaFeed feed = mFeed;
206 int lastDayBlock = -1;
211 if (mState == GridLayer.STATE_GRID_VIEW) {
212 set = feed.getFilteredSet();
214 set = feed.getCurrentSet();
218 if (!feed.hasExpandedMediaSet()) {
221 set = new MediaSet();
222 int numSlots = feed.getNumSlots();
223 for (int i = 0; i < numSlots; ++i) {
224 MediaSet slotSet = feed.getSetForSlot(i);
225 if (slotSet != null) {
226 ArrayList<MediaItem> slotSetItems = slotSet.getItems();
227 if (slotSetItems != null && slotSet.getNumItems() > 0) {
228 MediaItem item = slotSetItems.get(0);
237 GregorianCalendar time = new GregorianCalendar();
238 ArrayList<MediaItem> items = set.getItems();
241 while (j < set.getNumItems()) {
242 final MediaItem item = items.get(j);
245 time.setTimeInMillis(item.mDateTakenInMs);
246 // Detect year rollovers.
247 final int year = time.get(Calendar.YEAR);
248 if (year != lastYear) {
253 Marker marker = null;
254 // Detect month rollovers and emit a month marker.
255 final int month = time.get(Calendar.MONTH);
256 final int dayBlock = time.get(Calendar.DATE);
257 if (month != lastMonth) {
260 marker = new Marker(mMonthLabels[month], dx, time.getTimeInMillis(), year, month, dayBlock,
261 Marker.TYPE_MONTH, increment);
262 dx = addMarker(marker);
263 } else if (dayBlock != lastDayBlock) {
264 lastDayBlock = dayBlock;
266 marker = new Marker(mDayLabels[dayBlock], dx, time.getTimeInMillis(), year, month, dayBlock,
267 Marker.TYPE_DAY, increment);
268 dx = addMarker(marker);
271 marker = new Marker(mDot, dx, time.getTimeInMillis(), year, month, dayBlock, Marker.TYPE_DOT, increment);
272 dx = addMarker(marker);
274 for (int k = 0; k < increment; ++k) {
278 if (index >= items.size())
280 if (index == items.size() - 1 && k != 0)
282 MediaItem thisItem = items.get(index);
283 marker.items.add(thisItem);
284 mTracker.put(thisItem, marker);
286 if (j == items.size() - 1)
289 if (j >= items.size() - 1)
290 j = items.size() - 1;
293 mTotalWidth = dx - MARKER_SPACING_PIXELS * Gallery.PIXEL_DENSITY;
295 mPosition = getPositionForScroll(scrollX);
296 mPositionAnim = mPosition;
297 synchronized (mMarkersCopy) {
298 int numMarkers = mMarkers.size();
299 mMarkersCopy.clear();
300 mMarkersCopy.ensureCapacity(numMarkers);
301 for (int i = 0; i < numMarkers; ++i) {
302 mMarkersCopy.add(mMarkers.get(i));
308 private float addMarker(Marker marker) {
309 mMarkers.add(marker);
310 return marker.x + MARKER_SPACING_PIXELS * Gallery.PIXEL_DENSITY;
314 * private float getKnobXForPosition(float position) { return position * (mTotalWidth - mKnob.getWidth()); }
316 * private float getPositionForKnobX(float knobX) { return Math.max(0f, Math.min(1f, knobX / (mTotalWidth - mKnob.getWidth())));
319 * private float getScrollForPosition(float position) { return position * (mTotalWidth - mWidth);// - (1f - 2f * position) *
320 * MARKER_SPACING_PIXELS; }
323 private float getScrollForPosition(float position) {
324 // Map position [0, 1] to scroll [-visibleWidth/2, totalWidth -
326 // This has the effect of centering the scroll knob on screen.
327 float halfWidth = mWidth * 0.5f;
328 float positionInv = 1f - position;
329 float centered = positionInv * -halfWidth + position * (mTotalWidth - halfWidth);
333 private float getPositionForScroll(float scroll) {
334 float halfWidth = mWidth * 0.5f;
335 if (mTotalWidth == 0)
337 return ((scroll + halfWidth) / (mTotalWidth));
340 private float getKnobXForPosition(float position) {
341 return position * mTotalWidth;
344 private float getPositionForKnobX(float knobX) {
345 float normKnobX = (mTotalWidth == 0) ? 0 : knobX / mTotalWidth;
346 return Math.max(0f, Math.min(1f, normKnobX));
350 public boolean update(RenderView view, float dt) {
351 // Update animations.
352 final float ratio = Math.min(1f, 10f * dt);
353 final float invRatio = 1f - ratio;
354 mPositionAnim = ratio * mPosition + invRatio * mPositionAnim;
355 mScrollAnim = ratio * mScroll + invRatio * mScrollAnim;
356 // Handle autoscroll.
358 final float x = getKnobXForPosition(mPosition) - mScrollAnim;
360 float autoScrollMargin = AUTO_SCROLL_MARGIN * Gallery.PIXEL_DENSITY;
361 if (x < autoScrollMargin) {
362 velocity = -(float) Math.pow((1f - x / autoScrollMargin), 2);
363 } else if (x > mWidth - autoScrollMargin) {
364 velocity = (float) Math.pow(1f - (mWidth - x) / autoScrollMargin, 2);
368 mScroll += velocity * 400f * dt;
369 mPosition = getPositionForKnobX(mDragX + mScroll);
374 mAnimTextAlpha = FloatUtils.animate(mAnimTextAlpha, mTextAlpha, dt);
375 return mAnimTextAlpha != mTextAlpha;
379 public void renderBlended(RenderView view, GL11 gl) {
380 final float originX = mX;
381 final float originY = mY;
382 final float scrollOffset = mScrollAnim;
383 final float scrolledOriginX = originX - scrollOffset;
384 final float position = mPositionAnim;
385 final int knobId = mInDrag ? KNOB_PRESSED : KNOB;
386 final Texture knob = view.getResource(knobId);
387 // Draw the scroller knob.
389 if (view.bind(knob)) {
390 final float knobWidth = knob.getWidth();
391 view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, originY, 0f, knobWidth, knob
395 if (view.bind(knob)) {
396 final float knobWidth = knob.getWidth();
397 final float knobHeight = knob.getHeight();
398 view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, view.getHeight() - knobHeight, 0f,
399 knobWidth, knobHeight);
401 // we draw the current time on top of the knob
402 if (mInDrag || mAnimTextAlpha != 0.0f) {
403 Marker anchor = getAnchorMarker();
404 if (anchor != null) {
405 Texture month = mMonthLabels[anchor.month];
406 Texture day = mOpaqueDayLabels[anchor.day];
407 Texture year = getYearLabel(anchor.year);
408 boolean validDate = true;
409 if (anchor.year <= 1970) {
410 month = mDateUnknown;
415 view.loadTexture(month);
417 view.loadTexture(day);
418 view.loadTexture(year);
420 int numPixelsBufferX = 70;
421 float expectedWidth = month.getWidth()
422 + ((validDate) ? (day.getWidth() + year.getWidth() + 10 * Gallery.PIXEL_DENSITY) : 0);
423 if ((expectedWidth + numPixelsBufferX * Gallery.PIXEL_DENSITY) != mBackgroundRect.right) {
424 mBackgroundRect.right = (int) (expectedWidth + numPixelsBufferX * Gallery.PIXEL_DENSITY);
425 mBackgroundRect.bottom = (int) (month.getHeight() + 20 * Gallery.PIXEL_DENSITY);
427 Bitmap bitmap = Bitmap.createBitmap(mBackgroundRect.right, mBackgroundRect.bottom,
428 Bitmap.Config.ARGB_8888);
429 Canvas canvas = new Canvas();
430 canvas.setBitmap(bitmap);
431 mBackground.draw(canvas, mBackgroundRect, SRC_PAINT);
432 mBackgroundTexture = new BitmapTexture(bitmap);
433 view.loadTexture(mBackgroundTexture);
435 } catch (OutOfMemoryError e) {
439 gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
440 gl.glColor4f(mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha);
441 float x = (view.getWidth() - expectedWidth - numPixelsBufferX * Gallery.PIXEL_DENSITY) / 2;
442 float y = (view.getHeight() - 10 * Gallery.PIXEL_DENSITY) * 0.5f;
443 if (mBackgroundTexture != null) {
444 view.draw2D(mBackgroundTexture, x, y);
446 y = view.getHeight() * 0.5f;
447 x = (view.getWidth() - expectedWidth) / 2;
448 view.draw2D(month, x, y);
450 x += month.getWidth() + 3 * Gallery.PIXEL_DENSITY;
451 view.draw2D(day, x, y);
452 x += day.getWidth() + 7 * Gallery.PIXEL_DENSITY;
453 view.draw2D(year, x, y);
455 if (mAnimTextAlpha != 1f) {
456 gl.glColor4f(1f, 1f, 1f, 1f);
458 gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE);
465 public boolean onTouchEvent(MotionEvent event) {
466 // Set position on touch movement.
467 mDragX = event.getX();
468 mPosition = getPositionForKnobX(mDragX + mScroll);
470 // Notify the listener.
471 if (mListener != null) {
472 mListener.onTimeChanged(this);
475 // Update state when touch begins and ends.
476 switch (event.getAction()) {
477 case MotionEvent.ACTION_DOWN:
480 case MotionEvent.ACTION_UP:
481 case MotionEvent.ACTION_CANCEL:
482 // mScroll = getScrollForPosition(mPosition);
485 // Clamp to the nearest marker.
494 public void onFeedChanged(MediaFeed feed, boolean needsLayout) {
498 private static final class Marker {
499 Marker(StringTexture texture, float x, long time, int year, int month, int day, int type, int expectedCapacity) {
504 this.items = new ArrayList<MediaItem>(expectedCapacity);
507 public static final int TYPE_MONTH = 1;
508 public static final int TYPE_DAY = 2;
509 public static final int TYPE_DOT = 3;
510 public ArrayList<MediaItem> items;
511 public final float x;
512 public final int year;
513 public final int month;
514 public final int day;
518 public void generate(RenderView view, Lists lists) {
519 lists.updateList.add(this);
520 lists.blendedList.add(this);
521 lists.hitTestList.add(this);
524 public void onFeedAboutToChange(MediaFeed feed) {
525 // nothing needs to be done
529 private StringTexture getYearLabel(int year) {
532 StringTexture label = mYearLabels.get(year);
534 label = new StringTexture(Integer.toString(year), mMonthYearFormat);
535 mYearLabels.put(year, label);
540 public boolean isDragged() {