1 /* 2 * Copyright (C) 2009 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 17 package com.cooliris.media; 18 19 import java.util.ArrayList; 20 import java.util.Calendar; 21 import java.util.GregorianCalendar; 22 import java.util.HashMap; 23 24 import javax.microedition.khronos.opengles.GL11; 25 26 import android.content.Context; 27 import android.graphics.Bitmap; 28 import android.graphics.BitmapFactory; 29 import android.graphics.Canvas; 30 import android.graphics.NinePatch; 31 import android.graphics.Paint; 32 import android.graphics.PorterDuff; 33 import android.graphics.PorterDuffXfermode; 34 import android.graphics.Rect; 35 import android.util.SparseArray; 36 import android.view.MotionEvent; 37 38 import com.cooliris.app.App; 39 import com.cooliris.app.Res; 40 import com.cooliris.media.RenderView.Lists; 41 42 public final class TimeBar extends Layer implements MediaFeed.Listener { 43 public static final int HEIGHT = 48; 44 private static final int MARKER_SPACING_PIXELS = 50; 45 private static final float AUTO_SCROLL_MARGIN = 100f; 46 private static final Paint SRC_PAINT = new Paint(); 47 private Listener mListener = null; 48 private MediaFeed mFeed = null; 49 private float mTotalWidth = 0f; 50 private float mPosition = 0f; 51 private float mPositionAnim = 0f; 52 private float mScroll = 0f; 53 private float mScrollAnim = 0f; 54 private boolean mInDrag = false; 55 private float mDragX = 0f; 56 57 private ArrayList<Marker> mMarkers = new ArrayList<Marker>(); 58 private ArrayList<Marker> mMarkersCopy = new ArrayList<Marker>(); 59 60 private static final int KNOB = Res.drawable.scroller_new; 61 private static final int KNOB_PRESSED = Res.drawable.scroller_pressed_new; 62 private final StringTexture.Config mMonthYearFormat = new StringTexture.Config(); 63 private final StringTexture.Config mDayFormat = new StringTexture.Config(); 64 private final SparseArray<StringTexture> mYearLabels = new SparseArray<StringTexture>(); 65 private StringTexture mDateUnknown; 66 private final StringTexture[] mMonthLabels = new StringTexture[12]; 67 private final StringTexture[] mDayLabels = new StringTexture[32]; 68 private final StringTexture[] mOpaqueDayLabels = new StringTexture[32]; 69 private final StringTexture mDot = new StringTexture(""); 70 private final HashMap<MediaItem, Marker> mTracker = new HashMap<MediaItem, Marker>(1024); 71 private int mState; 72 private float mTextAlpha = 0.0f; 73 private float mAnimTextAlpha = 0.0f; 74 private boolean mShowTime; 75 private NinePatch mBackground; 76 private Rect mBackgroundRect; 77 private BitmapTexture mBackgroundTexture; 78 79 public interface Listener { 80 public void onTimeChanged(TimeBar timebar); 81 } 82 83 TimeBar(Context context) { 84 // Setup formatting for text labels. 85 mMonthYearFormat.fontSize = 17f * App.PIXEL_DENSITY; 86 mMonthYearFormat.bold = true; 87 mMonthYearFormat.a = 0.85f; 88 mDayFormat.fontSize = 17f * App.PIXEL_DENSITY; 89 mDayFormat.a = 0.61f; 90 regenerateStringsForContext(context); 91 Bitmap background = BitmapFactory.decodeResource(context.getResources(), Res.drawable.popup); 92 mBackground = new NinePatch(background, background.getNinePatchChunk(), null); 93 mBackgroundRect = new Rect(); 94 SRC_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); 95 } 96 97 public void regenerateStringsForContext(Context context) { 98 // Create textures for month names. 99 String[] months = context.getResources().getStringArray(Res.array.months_abbreviated); 100 for (int i = 0; i < months.length; ++i) { 101 mMonthLabels[i] = new StringTexture(months[i], mMonthYearFormat); 102 } 103 104 for (int i = 0; i <= 31; ++i) { 105 mDayLabels[i] = new StringTexture(Integer.toString(i), mDayFormat); 106 mOpaqueDayLabels[i] = new StringTexture(Integer.toString(i), mMonthYearFormat); 107 } 108 mDateUnknown = new StringTexture(context.getResources().getString(Res.string.date_unknown), mMonthYearFormat); 109 mBackgroundTexture = null; 110 } 111 112 public void setListener(Listener listener) { 113 mListener = listener; 114 } 115 116 public void setFeed(MediaFeed feed, int state, boolean needsLayout) { 117 mFeed = feed; 118 mState = state; 119 layout(); 120 if (needsLayout) { 121 mPosition = 0; 122 mScroll = getScrollForPosition(mPosition); 123 } 124 } 125 126 @Override 127 protected void onSizeChanged() { 128 mScroll = getScrollForPosition(mPosition); 129 } 130 131 public MediaItem getItem() { 132 synchronized (mMarkers) { 133 // x is between 0 and 1.0f 134 int numMarkers = mMarkers.size(); 135 if (numMarkers == 0) 136 return null; 137 int index = (int) (mPosition * (numMarkers)); 138 if (index >= numMarkers) 139 index = numMarkers - 1; 140 Marker marker = mMarkers.get(index); 141 if (marker != null) { 142 // we have to find the index of the media item depending upon 143 // the value of mPosition 144 float deltaBetweenMarkers = 1.0f / numMarkers; 145 float increment = mPosition - index * deltaBetweenMarkers; 146 // if (increment > deltaBetweenMarkers) 147 // increment = deltaBetweenMarkers; 148 // if (increment < 0) 149 // increment = 0; 150 ArrayList<MediaItem> items = marker.items; 151 int numItems = items.size(); 152 if (numItems == 0) 153 return null; 154 int itemIndex = (int) ((numItems) * increment / deltaBetweenMarkers); 155 if (itemIndex >= numItems) 156 itemIndex = numItems - 1; 157 return marker.items.get(itemIndex); 158 } 159 } 160 return null; 161 } 162 163 private Marker getAnchorMarker() { 164 synchronized (mMarkers) { 165 // x is between 0 and 1.0f 166 int numMarkers = mMarkers.size(); 167 if (numMarkers == 0) 168 return null; 169 int index = (int) (mPosition * (numMarkers)); 170 if (index >= numMarkers) 171 index = numMarkers - 1; 172 Marker marker = mMarkers.get(index); 173 return marker; 174 } 175 } 176 177 public void setItem(MediaItem item) { 178 Marker marker = mTracker.get(item); 179 if (marker != null) { 180 float markerX = (mTotalWidth == 0.0f) ? 0.0f : marker.x / mTotalWidth; 181 mPosition = Math.max(0.0f, Math.min(1.0f, markerX)); 182 mScroll = getScrollForPosition(mPosition); 183 } 184 } 185 186 @SuppressWarnings("unchecked") 187 private void layout() { 188 if (mFeed != null) { 189 // Clear existing markers. 190 mTracker.clear(); 191 synchronized (mMarkers) { 192 mMarkers.clear(); 193 } 194 float scrollX = mScroll; 195 // Place markers for every time interval that intersects one of the 196 // clusters. 197 // Markers for a full month would be for example: Jan 5 10 15 20 25 198 // 30. 199 MediaFeed feed = mFeed; 200 int lastYear = -1; 201 int lastMonth = -1; 202 int lastDayBlock = -1; 203 float dx = 0f; 204 int increment = 12; 205 MediaSet set = null; 206 mShowTime = true; 207 if (mState == GridLayer.STATE_GRID_VIEW) { 208 set = feed.getFilteredSet(); 209 if (set == null) { 210 set = feed.getCurrentSet(); 211 } 212 } else { 213 increment = 2; 214 if (!feed.hasExpandedMediaSet()) { 215 mShowTime = false; 216 } 217 set = new MediaSet(); 218 int numSlots = feed.getNumSlots(); 219 for (int i = 0; i < numSlots; ++i) { 220 MediaSet slotSet = feed.getSetForSlot(i); 221 if (slotSet != null) { 222 ArrayList<MediaItem> slotSetItems = slotSet.getItems(); 223 if (slotSetItems != null && slotSet.getNumItems() > 0) { 224 MediaItem item = slotSetItems.get(0); 225 if (item != null) { 226 set.addItem(item); 227 } 228 } 229 } 230 } 231 } 232 if (set != null) { 233 GregorianCalendar time = new GregorianCalendar(); 234 ArrayList<MediaItem> items = set.getItems(); 235 if (items != null) { 236 items = (ArrayList<MediaItem>)items.clone(); 237 int j = 0; 238 while (j < set.getNumItems()) { 239 final MediaItem item = items.get(j); 240 if (item == null) 241 continue; 242 time.setTimeInMillis(item.mDateTakenInMs); 243 // Detect year rollovers. 244 final int year = time.get(Calendar.YEAR); 245 if (year != lastYear) { 246 lastYear = year; 247 lastMonth = -1; 248 lastDayBlock = -1; 249 } 250 Marker marker = null; 251 // Detect month rollovers and emit a month marker. 252 final int month = time.get(Calendar.MONTH); 253 final int dayBlock = time.get(Calendar.DATE); 254 if (month != lastMonth) { 255 lastMonth = month; 256 lastDayBlock = -1; 257 marker = new Marker(dx, time.getTimeInMillis(), year, month, dayBlock, Marker.TYPE_MONTH, increment); 258 dx = addMarker(marker); 259 } else if (dayBlock != lastDayBlock) { 260 lastDayBlock = dayBlock; 261 if (dayBlock != 0) { 262 marker = new Marker(dx, time.getTimeInMillis(), year, month, dayBlock, Marker.TYPE_DAY, increment); 263 dx = addMarker(marker); 264 } 265 } else { 266 marker = new Marker(dx, time.getTimeInMillis(), year, month, dayBlock, Marker.TYPE_DOT, increment); 267 dx = addMarker(marker); 268 } 269 for (int k = 0; k < increment; ++k) { 270 int index = k + j; 271 if (index < 0) 272 continue; 273 if (index >= items.size()) 274 break; 275 if (index == items.size() - 1 && k != 0) 276 break; 277 MediaItem thisItem = items.get(index); 278 marker.items.add(thisItem); 279 mTracker.put(thisItem, marker); 280 } 281 if (j == items.size() - 1) 282 break; 283 j += increment; 284 if (j >= items.size() - 1) 285 j = items.size() - 1; 286 } 287 } 288 mTotalWidth = dx - MARKER_SPACING_PIXELS * App.PIXEL_DENSITY; 289 } 290 mPosition = getPositionForScroll(scrollX); 291 mPositionAnim = mPosition; 292 synchronized (mMarkersCopy) { 293 int numMarkers = mMarkers.size(); 294 mMarkersCopy.clear(); 295 mMarkersCopy.ensureCapacity(numMarkers); 296 for (int i = 0; i < numMarkers; ++i) { 297 mMarkersCopy.add(mMarkers.get(i)); 298 } 299 } 300 } 301 } 302 303 private float addMarker(Marker marker) { 304 mMarkers.add(marker); 305 return marker.x + MARKER_SPACING_PIXELS * App.PIXEL_DENSITY; 306 } 307 308 /* 309 * private float getKnobXForPosition(float position) { return position * 310 * (mTotalWidth - mKnob.getWidth()); } 311 * 312 * private float getPositionForKnobX(float knobX) { return Math.max(0f, 313 * Math.min(1f, knobX / (mTotalWidth - mKnob.getWidth()))); } 314 * 315 * private float getScrollForPosition(float position) { return position * 316 * (mTotalWidth - mWidth);// - (1f - 2f * position) * MARKER_SPACING_PIXELS; 317 * } 318 */ 319 320 private float getScrollForPosition(float position) { 321 // Map position [0, 1] to scroll [-visibleWidth/2, totalWidth - 322 // visibleWidth/2]. 323 // This has the effect of centering the scroll knob on screen. 324 float halfWidth = mWidth * 0.5f; 325 float positionInv = 1f - position; 326 float centered = positionInv * -halfWidth + position * (mTotalWidth - halfWidth); 327 return centered; 328 } 329 330 private float getPositionForScroll(float scroll) { 331 float halfWidth = mWidth * 0.5f; 332 if (mTotalWidth == 0) 333 return 0; 334 return ((scroll + halfWidth) / (mTotalWidth)); 335 } 336 337 private float getKnobXForPosition(float position) { 338 return position * mTotalWidth; 339 } 340 341 private float getPositionForKnobX(float knobX) { 342 float normKnobX = (mTotalWidth == 0) ? 0 : knobX / mTotalWidth; 343 return Math.max(0f, Math.min(1f, normKnobX)); 344 } 345 346 @Override 347 public boolean update(RenderView view, float dt) { 348 // Update animations. 349 final float ratio = Math.min(1f, 10f * dt); 350 final float invRatio = 1f - ratio; 351 mPositionAnim = ratio * mPosition + invRatio * mPositionAnim; 352 mScrollAnim = ratio * mScroll + invRatio * mScrollAnim; 353 // Handle autoscroll. 354 if (mInDrag) { 355 final float x = getKnobXForPosition(mPosition) - mScrollAnim; 356 float velocity; 357 float autoScrollMargin = AUTO_SCROLL_MARGIN * App.PIXEL_DENSITY; 358 if (x < autoScrollMargin) { 359 velocity = -(float) Math.pow((1f - x / autoScrollMargin), 2); 360 } else if (x > mWidth - autoScrollMargin) { 361 velocity = (float) Math.pow(1f - (mWidth - x) / autoScrollMargin, 2); 362 } else { 363 velocity = 0; 364 } 365 mScroll += velocity * 400f * dt; 366 mPosition = getPositionForKnobX(mDragX + mScroll); 367 mTextAlpha = 1.0f; 368 } else { 369 mTextAlpha = 0.0f; 370 } 371 mAnimTextAlpha = FloatUtils.animate(mAnimTextAlpha, mTextAlpha, dt); 372 return mAnimTextAlpha != mTextAlpha; 373 } 374 375 @Override 376 public void renderBlended(RenderView view, GL11 gl) { 377 final float originX = mX; 378 final float originY = mY; 379 final float scrollOffset = mScrollAnim; 380 final float scrolledOriginX = originX - scrollOffset; 381 final float position = mPositionAnim; 382 final int knobId = mInDrag ? KNOB_PRESSED : KNOB; 383 final Texture knob = view.getResource(knobId); 384 // Draw the scroller knob. 385 if (!mShowTime) { 386 if (view.bind(knob)) { 387 final float knobWidth = knob.getWidth(); 388 view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, originY, 0f, knobWidth, knob 389 .getHeight()); 390 } 391 } else { 392 if (view.bind(knob)) { 393 final float knobWidth = knob.getWidth(); 394 final float knobHeight = knob.getHeight(); 395 view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, view.getHeight() - knobHeight, 0f, 396 knobWidth, knobHeight); 397 } 398 // we draw the current time on top of the knob 399 if (mInDrag || mAnimTextAlpha != 0.0f) { 400 Marker anchor = getAnchorMarker(); 401 if (anchor != null) { 402 Texture month = mMonthLabels[anchor.month]; 403 Texture day = mOpaqueDayLabels[anchor.day]; 404 Texture year = getYearLabel(anchor.year); 405 boolean validDate = true; 406 if (anchor.year <= 1970) { 407 month = mDateUnknown; 408 day = null; 409 year = null; 410 validDate = false; 411 } 412 view.loadTexture(month); 413 if (validDate) { 414 view.loadTexture(day); 415 view.loadTexture(year); 416 } 417 int numPixelsBufferX = 70; 418 float expectedWidth = month.getWidth() 419 + ((validDate) ? (day.getWidth() + year.getWidth() + 10 * App.PIXEL_DENSITY) : 0); 420 if ((expectedWidth + numPixelsBufferX * App.PIXEL_DENSITY) != mBackgroundRect.right) { 421 mBackgroundRect.right = (int) (expectedWidth + numPixelsBufferX * App.PIXEL_DENSITY); 422 mBackgroundRect.bottom = (int) (month.getHeight() + 20 * App.PIXEL_DENSITY); 423 try { 424 Bitmap bitmap = Bitmap.createBitmap(mBackgroundRect.right, mBackgroundRect.bottom, 425 Bitmap.Config.ARGB_8888); 426 Canvas canvas = new Canvas(); 427 canvas.setBitmap(bitmap); 428 mBackground.draw(canvas, mBackgroundRect, SRC_PAINT); 429 mBackgroundTexture = new BitmapTexture(bitmap); 430 view.loadTexture(mBackgroundTexture); 431 bitmap.recycle(); 432 } catch (OutOfMemoryError e) { 433 // Do nothing. 434 } 435 } 436 gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE); 437 gl.glColor4f(mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha); 438 float x = (view.getWidth() - expectedWidth - numPixelsBufferX * App.PIXEL_DENSITY) / 2; 439 float y = (view.getHeight() - 10 * App.PIXEL_DENSITY) * 0.5f; 440 if (mBackgroundTexture != null) { 441 view.draw2D(mBackgroundTexture, x, y); 442 } 443 y = view.getHeight() * 0.5f; 444 x = (view.getWidth() - expectedWidth) / 2; 445 view.draw2D(month, x, y); 446 if (validDate) { 447 x += month.getWidth() + 3 * App.PIXEL_DENSITY; 448 view.draw2D(day, x, y); 449 x += day.getWidth() + 7 * App.PIXEL_DENSITY; 450 view.draw2D(year, x, y); 451 } 452 if (mAnimTextAlpha != 1f) { 453 gl.glColor4f(1f, 1f, 1f, 1f); 454 } 455 gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); 456 } 457 } 458 } 459 } 460 461 @Override 462 public boolean onTouchEvent(MotionEvent event) { 463 // Set position on touch movement. 464 mDragX = event.getX(); 465 mPosition = getPositionForKnobX(mDragX + mScroll); 466 467 // Notify the listener. 468 if (mListener != null) { 469 mListener.onTimeChanged(this); 470 } 471 472 // Update state when touch begins and ends. 473 switch (event.getAction()) { 474 case MotionEvent.ACTION_DOWN: 475 mInDrag = true; 476 break; 477 case MotionEvent.ACTION_UP: 478 case MotionEvent.ACTION_CANCEL: 479 // mScroll = getScrollForPosition(mPosition); 480 mInDrag = false; 481 482 // Clamp to the nearest marker. 483 setItem(getItem()); 484 default: 485 break; 486 } 487 488 return true; 489 } 490 491 public void onFeedChanged(MediaFeed feed, boolean needsLayout) { 492 layout(); 493 } 494 495 private static final class Marker { 496 Marker(float x, long time, int year, int month, int day, int type, int expectedCapacity) { 497 this.x = x; 498 this.year = year; 499 this.month = month; 500 this.day = day; 501 this.items = new ArrayList<MediaItem>(expectedCapacity); 502 } 503 504 public static final int TYPE_MONTH = 1; 505 public static final int TYPE_DAY = 2; 506 public static final int TYPE_DOT = 3; 507 public ArrayList<MediaItem> items; 508 public final float x; 509 public final int year; 510 public final int month; 511 public final int day; 512 } 513 514 @Override 515 public void generate(RenderView view, Lists lists) { 516 lists.updateList.add(this); 517 lists.blendedList.add(this); 518 lists.hitTestList.add(this); 519 } 520 521 public void onFeedAboutToChange(MediaFeed feed) { 522 // nothing needs to be done 523 return; 524 } 525 526 private StringTexture getYearLabel(int year) { 527 if (year <= 1970) 528 return mDot; 529 StringTexture label = mYearLabels.get(year); 530 if (label == null) { 531 label = new StringTexture(Integer.toString(year), mMonthYearFormat); 532 mYearLabels.put(year, label); 533 } 534 return label; 535 } 536 537 public boolean isDragged() { 538 return mInDrag; 539 } 540 } 541