Home | History | Annotate | Download | only in media
      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