| package com.cooliris.media; |
| |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.GregorianCalendar; |
| import java.util.HashMap; |
| |
| import javax.microedition.khronos.opengles.GL11; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapFactory; |
| import android.graphics.Canvas; |
| import android.graphics.NinePatch; |
| import android.graphics.Paint; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.Rect; |
| import android.util.SparseArray; |
| import android.view.MotionEvent; |
| import com.cooliris.media.RenderView.Lists; |
| |
| public final class TimeBar extends Layer implements MediaFeed.Listener { |
| public static final int HEIGHT = 48; |
| private static final int MARKER_SPACING_PIXELS = 50; |
| private static final float AUTO_SCROLL_MARGIN = 100f; |
| private static final Paint SRC_PAINT = new Paint(); |
| private Listener mListener = null; |
| private MediaFeed mFeed = null; |
| private float mTotalWidth = 0f; |
| private float mPosition = 0f; |
| private float mPositionAnim = 0f; |
| private float mScroll = 0f; |
| private float mScrollAnim = 0f; |
| private boolean mInDrag = false; |
| private float mDragX = 0f; |
| |
| private ArrayList<Marker> mMarkers = new ArrayList<Marker>(); |
| private ArrayList<Marker> mMarkersCopy = new ArrayList<Marker>(); |
| |
| private static final int KNOB = R.drawable.scroller_new; |
| private static final int KNOB_PRESSED = R.drawable.scroller_pressed_new; |
| private final StringTexture.Config mMonthYearFormat = new StringTexture.Config(); |
| private final StringTexture.Config mDayFormat = new StringTexture.Config(); |
| private final SparseArray<StringTexture> mYearLabels = new SparseArray<StringTexture>(); |
| private StringTexture mDateUnknown; |
| private final StringTexture[] mMonthLabels = new StringTexture[12]; |
| private final StringTexture[] mDayLabels = new StringTexture[32]; |
| private final StringTexture[] mOpaqueDayLabels = new StringTexture[32]; |
| private final StringTexture mDot = new StringTexture("¥"); |
| private final HashMap<MediaItem, Marker> mTracker = new HashMap<MediaItem, Marker>(1024); |
| private int mState; |
| private float mTextAlpha = 0.0f; |
| private float mAnimTextAlpha = 0.0f; |
| private boolean mShowTime; |
| private NinePatch mBackground; |
| private Rect mBackgroundRect; |
| private BitmapTexture mBackgroundTexture; |
| |
| public interface Listener { |
| public void onTimeChanged(TimeBar timebar); |
| } |
| |
| TimeBar(Context context) { |
| // Setup formatting for text labels. |
| mMonthYearFormat.fontSize = 17f * Gallery.PIXEL_DENSITY; |
| mMonthYearFormat.bold = true; |
| mMonthYearFormat.a = 0.85f; |
| mDayFormat.fontSize = 17f * Gallery.PIXEL_DENSITY; |
| mDayFormat.a = 0.61f; |
| regenerateStringsForContext(context); |
| Bitmap background = BitmapFactory.decodeResource(context.getResources(), R.drawable.popup); |
| mBackground = new NinePatch(background, background.getNinePatchChunk(), null); |
| mBackgroundRect = new Rect(); |
| SRC_PAINT.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC)); |
| } |
| |
| public void regenerateStringsForContext(Context context) { |
| // Create textures for month names. |
| String[] months = context.getResources().getStringArray(R.array.months_abbreviated); |
| for (int i = 0; i < months.length; ++i) { |
| mMonthLabels[i] = new StringTexture(months[i], mMonthYearFormat); |
| } |
| |
| for (int i = 0; i <= 31; ++i) { |
| mDayLabels[i] = new StringTexture(Integer.toString(i), mDayFormat); |
| mOpaqueDayLabels[i] = new StringTexture(Integer.toString(i), mMonthYearFormat); |
| } |
| mDateUnknown = new StringTexture(context.getResources().getString(R.string.date_unknown), mMonthYearFormat); |
| mBackgroundTexture = null; |
| } |
| |
| public void setListener(Listener listener) { |
| mListener = listener; |
| } |
| |
| public void setFeed(MediaFeed feed, int state, boolean needsLayout) { |
| mFeed = feed; |
| mState = state; |
| layout(); |
| if (needsLayout) { |
| mPosition = 0; |
| mScroll = getScrollForPosition(mPosition); |
| } |
| } |
| |
| @Override |
| protected void onSizeChanged() { |
| mScroll = getScrollForPosition(mPosition); |
| } |
| |
| public MediaItem getItem() { |
| synchronized (mMarkers) { |
| // x is between 0 and 1.0f |
| int numMarkers = mMarkers.size(); |
| if (numMarkers == 0) |
| return null; |
| int index = (int) (mPosition * (numMarkers)); |
| if (index >= numMarkers) |
| index = numMarkers - 1; |
| Marker marker = mMarkers.get(index); |
| if (marker != null) { |
| // we have to find the index of the media item depending upon |
| // the value of mPosition |
| float deltaBetweenMarkers = 1.0f / numMarkers; |
| float increment = mPosition - index * deltaBetweenMarkers; |
| // if (increment > deltaBetweenMarkers) |
| // increment = deltaBetweenMarkers; |
| // if (increment < 0) |
| // increment = 0; |
| ArrayList<MediaItem> items = marker.items; |
| int numItems = items.size(); |
| if (numItems == 0) |
| return null; |
| int itemIndex = (int) ((numItems) * increment / deltaBetweenMarkers); |
| if (itemIndex >= numItems) |
| itemIndex = numItems - 1; |
| return marker.items.get(itemIndex); |
| } |
| } |
| return null; |
| } |
| |
| private Marker getAnchorMarker() { |
| synchronized (mMarkers) { |
| // x is between 0 and 1.0f |
| int numMarkers = mMarkers.size(); |
| if (numMarkers == 0) |
| return null; |
| int index = (int) (mPosition * (numMarkers)); |
| if (index >= numMarkers) |
| index = numMarkers - 1; |
| Marker marker = mMarkers.get(index); |
| return marker; |
| } |
| } |
| |
| public void setItem(MediaItem item) { |
| Marker marker = mTracker.get(item); |
| if (marker != null) { |
| float markerX = (mTotalWidth == 0.0f) ? 0.0f : marker.x / mTotalWidth; |
| mPosition = Math.max(0.0f, Math.min(1.0f, markerX)); |
| mScroll = getScrollForPosition(mPosition); |
| } |
| } |
| |
| private void layout() { |
| if (mFeed != null) { |
| // Clear existing markers. |
| mTracker.clear(); |
| synchronized (mMarkers) { |
| mMarkers.clear(); |
| } |
| float scrollX = mScroll; |
| // Place markers for every time interval that intersects one of the |
| // clusters. |
| // Markers for a full month would be for example: Jan 5 10 15 20 25 |
| // 30. |
| MediaFeed feed = mFeed; |
| int lastYear = -1; |
| int lastMonth = -1; |
| int lastDayBlock = -1; |
| float dx = 0f; |
| int increment = 12; |
| MediaSet set = null; |
| mShowTime = true; |
| if (mState == GridLayer.STATE_GRID_VIEW) { |
| set = feed.getFilteredSet(); |
| if (set == null) { |
| set = feed.getCurrentSet(); |
| } |
| } else { |
| increment = 2; |
| if (!feed.hasExpandedMediaSet()) { |
| mShowTime = false; |
| } |
| set = new MediaSet(); |
| int numSlots = feed.getNumSlots(); |
| for (int i = 0; i < numSlots; ++i) { |
| MediaSet slotSet = feed.getSetForSlot(i); |
| if (slotSet != null) { |
| ArrayList<MediaItem> slotSetItems = slotSet.getItems(); |
| if (slotSetItems != null && slotSet.getNumItems() > 0) { |
| MediaItem item = slotSetItems.get(0); |
| if (item != null) { |
| set.addItem(item); |
| } |
| } |
| } |
| } |
| } |
| if (set != null) { |
| GregorianCalendar time = new GregorianCalendar(); |
| ArrayList<MediaItem> items = set.getItems(); |
| if (items != null) { |
| int j = 0; |
| while (j < set.getNumItems()) { |
| final MediaItem item = items.get(j); |
| if (item == null) |
| continue; |
| time.setTimeInMillis(item.mDateTakenInMs); |
| // Detect year rollovers. |
| final int year = time.get(Calendar.YEAR); |
| if (year != lastYear) { |
| lastYear = year; |
| lastMonth = -1; |
| lastDayBlock = -1; |
| } |
| Marker marker = null; |
| // Detect month rollovers and emit a month marker. |
| final int month = time.get(Calendar.MONTH); |
| final int dayBlock = time.get(Calendar.DATE); |
| if (month != lastMonth) { |
| lastMonth = month; |
| lastDayBlock = -1; |
| marker = new Marker(dx, time.getTimeInMillis(), year, month, dayBlock, |
| Marker.TYPE_MONTH, increment); |
| dx = addMarker(marker); |
| } else if (dayBlock != lastDayBlock) { |
| lastDayBlock = dayBlock; |
| if (dayBlock != 0) { |
| marker = new Marker(dx, time.getTimeInMillis(), year, month, dayBlock, |
| Marker.TYPE_DAY, increment); |
| dx = addMarker(marker); |
| } |
| } else { |
| marker = new Marker(dx, time.getTimeInMillis(), year, month, dayBlock, Marker.TYPE_DOT, increment); |
| dx = addMarker(marker); |
| } |
| for (int k = 0; k < increment; ++k) { |
| int index = k + j; |
| if (index < 0) |
| continue; |
| if (index >= items.size()) |
| break; |
| if (index == items.size() - 1 && k != 0) |
| break; |
| MediaItem thisItem = items.get(index); |
| marker.items.add(thisItem); |
| mTracker.put(thisItem, marker); |
| } |
| if (j == items.size() - 1) |
| break; |
| j += increment; |
| if (j >= items.size() - 1) |
| j = items.size() - 1; |
| } |
| } |
| mTotalWidth = dx - MARKER_SPACING_PIXELS * Gallery.PIXEL_DENSITY; |
| } |
| mPosition = getPositionForScroll(scrollX); |
| mPositionAnim = mPosition; |
| synchronized (mMarkersCopy) { |
| int numMarkers = mMarkers.size(); |
| mMarkersCopy.clear(); |
| mMarkersCopy.ensureCapacity(numMarkers); |
| for (int i = 0; i < numMarkers; ++i) { |
| mMarkersCopy.add(mMarkers.get(i)); |
| } |
| } |
| } |
| } |
| |
| private float addMarker(Marker marker) { |
| mMarkers.add(marker); |
| return marker.x + MARKER_SPACING_PIXELS * Gallery.PIXEL_DENSITY; |
| } |
| |
| /* |
| * private float getKnobXForPosition(float position) { return position * (mTotalWidth - mKnob.getWidth()); } |
| * |
| * private float getPositionForKnobX(float knobX) { return Math.max(0f, Math.min(1f, knobX / (mTotalWidth - mKnob.getWidth()))); |
| * } |
| * |
| * private float getScrollForPosition(float position) { return position * (mTotalWidth - mWidth);// - (1f - 2f * position) * |
| * MARKER_SPACING_PIXELS; } |
| */ |
| |
| private float getScrollForPosition(float position) { |
| // Map position [0, 1] to scroll [-visibleWidth/2, totalWidth - |
| // visibleWidth/2]. |
| // This has the effect of centering the scroll knob on screen. |
| float halfWidth = mWidth * 0.5f; |
| float positionInv = 1f - position; |
| float centered = positionInv * -halfWidth + position * (mTotalWidth - halfWidth); |
| return centered; |
| } |
| |
| private float getPositionForScroll(float scroll) { |
| float halfWidth = mWidth * 0.5f; |
| if (mTotalWidth == 0) |
| return 0; |
| return ((scroll + halfWidth) / (mTotalWidth)); |
| } |
| |
| private float getKnobXForPosition(float position) { |
| return position * mTotalWidth; |
| } |
| |
| private float getPositionForKnobX(float knobX) { |
| float normKnobX = (mTotalWidth == 0) ? 0 : knobX / mTotalWidth; |
| return Math.max(0f, Math.min(1f, normKnobX)); |
| } |
| |
| @Override |
| public boolean update(RenderView view, float dt) { |
| // Update animations. |
| final float ratio = Math.min(1f, 10f * dt); |
| final float invRatio = 1f - ratio; |
| mPositionAnim = ratio * mPosition + invRatio * mPositionAnim; |
| mScrollAnim = ratio * mScroll + invRatio * mScrollAnim; |
| // Handle autoscroll. |
| if (mInDrag) { |
| final float x = getKnobXForPosition(mPosition) - mScrollAnim; |
| float velocity; |
| float autoScrollMargin = AUTO_SCROLL_MARGIN * Gallery.PIXEL_DENSITY; |
| if (x < autoScrollMargin) { |
| velocity = -(float) Math.pow((1f - x / autoScrollMargin), 2); |
| } else if (x > mWidth - autoScrollMargin) { |
| velocity = (float) Math.pow(1f - (mWidth - x) / autoScrollMargin, 2); |
| } else { |
| velocity = 0; |
| } |
| mScroll += velocity * 400f * dt; |
| mPosition = getPositionForKnobX(mDragX + mScroll); |
| mTextAlpha = 1.0f; |
| } else { |
| mTextAlpha = 0.0f; |
| } |
| mAnimTextAlpha = FloatUtils.animate(mAnimTextAlpha, mTextAlpha, dt); |
| return mAnimTextAlpha != mTextAlpha; |
| } |
| |
| @Override |
| public void renderBlended(RenderView view, GL11 gl) { |
| final float originX = mX; |
| final float originY = mY; |
| final float scrollOffset = mScrollAnim; |
| final float scrolledOriginX = originX - scrollOffset; |
| final float position = mPositionAnim; |
| final int knobId = mInDrag ? KNOB_PRESSED : KNOB; |
| final Texture knob = view.getResource(knobId); |
| // Draw the scroller knob. |
| if (!mShowTime) { |
| if (view.bind(knob)) { |
| final float knobWidth = knob.getWidth(); |
| view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, originY, 0f, knobWidth, knob |
| .getHeight()); |
| } |
| } else { |
| if (view.bind(knob)) { |
| final float knobWidth = knob.getWidth(); |
| final float knobHeight = knob.getHeight(); |
| view.draw2D(scrolledOriginX + getKnobXForPosition(position) - knobWidth * 0.5f, view.getHeight() - knobHeight, 0f, |
| knobWidth, knobHeight); |
| } |
| // we draw the current time on top of the knob |
| if (mInDrag || mAnimTextAlpha != 0.0f) { |
| Marker anchor = getAnchorMarker(); |
| if (anchor != null) { |
| Texture month = mMonthLabels[anchor.month]; |
| Texture day = mOpaqueDayLabels[anchor.day]; |
| Texture year = getYearLabel(anchor.year); |
| boolean validDate = true; |
| if (anchor.year <= 1970) { |
| month = mDateUnknown; |
| day = null; |
| year = null; |
| validDate = false; |
| } |
| view.loadTexture(month); |
| if (validDate) { |
| view.loadTexture(day); |
| view.loadTexture(year); |
| } |
| int numPixelsBufferX = 70; |
| float expectedWidth = month.getWidth() |
| + ((validDate) ? (day.getWidth() + year.getWidth() + 10 * Gallery.PIXEL_DENSITY) : 0); |
| if ((expectedWidth + numPixelsBufferX * Gallery.PIXEL_DENSITY) != mBackgroundRect.right) { |
| mBackgroundRect.right = (int) (expectedWidth + numPixelsBufferX * Gallery.PIXEL_DENSITY); |
| mBackgroundRect.bottom = (int) (month.getHeight() + 20 * Gallery.PIXEL_DENSITY); |
| try { |
| Bitmap bitmap = Bitmap.createBitmap(mBackgroundRect.right, mBackgroundRect.bottom, |
| Bitmap.Config.ARGB_8888); |
| Canvas canvas = new Canvas(); |
| canvas.setBitmap(bitmap); |
| mBackground.draw(canvas, mBackgroundRect, SRC_PAINT); |
| mBackgroundTexture = new BitmapTexture(bitmap); |
| view.loadTexture(mBackgroundTexture); |
| bitmap.recycle(); |
| } catch (OutOfMemoryError e) { |
| // Do nothing. |
| } |
| } |
| gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE); |
| gl.glColor4f(mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha, mAnimTextAlpha); |
| float x = (view.getWidth() - expectedWidth - numPixelsBufferX * Gallery.PIXEL_DENSITY) / 2; |
| float y = (view.getHeight() - 10 * Gallery.PIXEL_DENSITY) * 0.5f; |
| if (mBackgroundTexture != null) { |
| view.draw2D(mBackgroundTexture, x, y); |
| } |
| y = view.getHeight() * 0.5f; |
| x = (view.getWidth() - expectedWidth) / 2; |
| view.draw2D(month, x, y); |
| if (validDate) { |
| x += month.getWidth() + 3 * Gallery.PIXEL_DENSITY; |
| view.draw2D(day, x, y); |
| x += day.getWidth() + 7 * Gallery.PIXEL_DENSITY; |
| view.draw2D(year, x, y); |
| } |
| if (mAnimTextAlpha != 1f) { |
| gl.glColor4f(1f, 1f, 1f, 1f); |
| } |
| gl.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_REPLACE); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent event) { |
| // Set position on touch movement. |
| mDragX = event.getX(); |
| mPosition = getPositionForKnobX(mDragX + mScroll); |
| |
| // Notify the listener. |
| if (mListener != null) { |
| mListener.onTimeChanged(this); |
| } |
| |
| // Update state when touch begins and ends. |
| switch (event.getAction()) { |
| case MotionEvent.ACTION_DOWN: |
| mInDrag = true; |
| break; |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| // mScroll = getScrollForPosition(mPosition); |
| mInDrag = false; |
| |
| // Clamp to the nearest marker. |
| setItem(getItem()); |
| default: |
| break; |
| } |
| |
| return true; |
| } |
| |
| public void onFeedChanged(MediaFeed feed, boolean needsLayout) { |
| layout(); |
| } |
| |
| private static final class Marker { |
| Marker(float x, long time, int year, int month, int day, int type, int expectedCapacity) { |
| this.x = x; |
| this.year = year; |
| this.month = month; |
| this.day = day; |
| this.items = new ArrayList<MediaItem>(expectedCapacity); |
| } |
| |
| public static final int TYPE_MONTH = 1; |
| public static final int TYPE_DAY = 2; |
| public static final int TYPE_DOT = 3; |
| public ArrayList<MediaItem> items; |
| public final float x; |
| public final int year; |
| public final int month; |
| public final int day; |
| } |
| |
| @Override |
| public void generate(RenderView view, Lists lists) { |
| lists.updateList.add(this); |
| lists.blendedList.add(this); |
| lists.hitTestList.add(this); |
| } |
| |
| public void onFeedAboutToChange(MediaFeed feed) { |
| // nothing needs to be done |
| return; |
| } |
| |
| private StringTexture getYearLabel(int year) { |
| if (year <= 1970) |
| return mDot; |
| StringTexture label = mYearLabels.get(year); |
| if (label == null) { |
| label = new StringTexture(Integer.toString(year), mMonthYearFormat); |
| mYearLabels.put(year, label); |
| } |
| return label; |
| } |
| |
| public boolean isDragged() { |
| return mInDrag; |
| } |
| } |