1/* 2 * Copyright (C) 2011 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.videoeditor; 18 19import android.content.Context; 20import android.content.res.Resources; 21import android.graphics.Bitmap; 22import android.graphics.BitmapFactory; 23import android.graphics.Canvas; 24import android.graphics.Color; 25import android.graphics.Paint; 26import android.graphics.Paint.Style; 27import android.graphics.Typeface; 28import android.media.videoeditor.VideoEditor; 29import android.os.AsyncTask; 30import android.text.format.DateUtils; 31import android.text.TextPaint; 32import android.text.TextUtils; 33import android.util.Log; 34import android.util.LruCache; 35import android.view.LayoutInflater; 36import android.view.View; 37import android.view.ViewGroup; 38import android.widget.BaseAdapter; 39import android.widget.ImageView; 40import android.widget.TextView; 41 42import com.android.videoeditor.service.VideoEditorProject; 43import com.android.videoeditor.util.ImageUtils; 44 45import java.io.File; 46import java.io.IOException; 47import java.util.List; 48 49 50public class ProjectPickerAdapter extends BaseAdapter { 51 private Context mContext; 52 private Resources mResources; 53 private LayoutInflater mInflater; 54 private List<VideoEditorProject> mProjects; 55 private int mItemWidth; 56 private int mItemHeight; 57 private int mOverlayHeight; 58 private int mOverlayVerticalInset; 59 private int mOverlayHorizontalInset; 60 private LruCache<String, Bitmap> mPreviewBitmapCache; 61 62 public ProjectPickerAdapter(Context context, LayoutInflater inflater, 63 List<VideoEditorProject> projects) { 64 mContext = context; 65 mResources = context.getResources(); 66 mInflater = inflater; 67 mProjects = projects; 68 mItemWidth = (int) mResources.getDimension(R.dimen.project_picker_item_width); 69 mItemHeight = (int) mResources.getDimension(R.dimen.project_picker_item_height); 70 mOverlayHeight = (int) mResources.getDimension( 71 R.dimen.project_picker_item_overlay_height); 72 mOverlayVerticalInset = (int) mResources.getDimension( 73 R.dimen.project_picker_item_overlay_vertical_inset); 74 mOverlayHorizontalInset = (int) mResources.getDimension( 75 R.dimen.project_picker_item_overlay_horizontal_inset); 76 // Limit the cache size to 15 thumbnails. 77 mPreviewBitmapCache = new LruCache<String, Bitmap>(15); 78 } 79 80 /** 81 * Clears project list and update display. 82 */ 83 public void clear() { 84 mPreviewBitmapCache.evictAll(); 85 mProjects.clear(); 86 notifyDataSetChanged(); 87 } 88 89 /** 90 * Removes the project with specified {@code projectPath} from the project list and updates the 91 * display. 92 * 93 * @param projectPath The project path of the to-be-removed project 94 * @return {@code true} if the project is successfully removed, 95 * {@code false} if no removal happened 96 */ 97 public boolean remove(String projectPath) { 98 for (VideoEditorProject project : mProjects) { 99 if (project.getPath().equals(projectPath)) { 100 if (mProjects.remove(project)) { 101 notifyDataSetChanged(); 102 return true; 103 } else { 104 return false; 105 } 106 } 107 } 108 return false; 109 } 110 111 @Override 112 public int getCount() { 113 // Add one to represent an additional dummy project for "create new project" option. 114 return mProjects.size() + 1; 115 } 116 117 @Override 118 public Object getItem(int position) { 119 if (position == mProjects.size()) { 120 return null; 121 } 122 return mProjects.get(position); 123 } 124 125 @Override 126 public long getItemId(int position) { 127 return position; 128 } 129 130 @Override 131 public View getView(int position, View convertView, ViewGroup parent) { 132 // Inflate a new view with project thumbnail and information. 133 // We never reuse convertView because we load thumbnails asynchronously 134 // and hook an async task with the new view. If the new view is reused 135 // as a convertView, the async task might put a wrong thumbnail on it. 136 View v = mInflater.inflate(R.layout.project_picker_item, null); 137 ImageView iv = (ImageView) v.findViewById(R.id.thumbnail); 138 Bitmap thumbnail; 139 String title; 140 String duration; 141 if (position == mProjects.size()) { 142 title = mContext.getString(R.string.projects_new_project); 143 duration = ""; 144 thumbnail = renderNewProjectThumbnail(); 145 } else { 146 VideoEditorProject project = mProjects.get(position); 147 title = project.getName(); 148 if (title == null) { 149 title = ""; 150 } 151 duration = millisecondsToTimeString(project.getProjectDuration()); 152 thumbnail = getThumbnail(project.getPath(), iv, title, duration); 153 } 154 155 if (thumbnail != null) { 156 drawBottomOverlay(thumbnail, title, duration); 157 iv.setImageBitmap(thumbnail); 158 } 159 160 return v; 161 } 162 163 /** 164 * Draws transparent black bottom overlay with movie title and duration on the bitmap. 165 */ 166 public void drawBottomOverlay(Bitmap bitmap, String title, String duration) { 167 // Draw overlay at the bottom of the canvas. 168 final Canvas canvas = new Canvas(bitmap); 169 final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); 170 paint.setColor(Color.BLACK); 171 paint.setAlpha(128); 172 final int left = 0, top = bitmap.getHeight() - mOverlayHeight, 173 right = bitmap.getWidth(), bottom = bitmap.getHeight(); 174 canvas.drawRect(left, top, right, bottom, paint); 175 176 paint.setColor(Color.WHITE); 177 paint.setTextSize((int) mResources.getDimension(R.dimen.project_picker_item_font_size)); 178 179 // Draw movie title at the left of the overlay. Trim title if it is going to overlap with 180 // duration text. 181 final int availableTitleWidth = bitmap.getWidth() - (int) paint.measureText(duration); 182 title = TextUtils.ellipsize(title, new TextPaint(paint), availableTitleWidth, 183 TextUtils.TruncateAt.END).toString(); 184 canvas.drawText(title, mOverlayHorizontalInset, 185 bitmap.getHeight() - mOverlayHeight + mOverlayVerticalInset, 186 paint); 187 188 // Draw movie duration at the right of the overlay. 189 canvas.drawText(duration, 190 bitmap.getWidth() - paint.measureText(duration) - mOverlayHorizontalInset, 191 bitmap.getHeight() - mOverlayHeight + mOverlayVerticalInset, 192 paint); 193 } 194 195 private Bitmap getThumbnail(String projectPath, ImageView imageView, String title, 196 String duration) { 197 Bitmap previewBitmap = mPreviewBitmapCache.get(projectPath); 198 if (previewBitmap == null) { 199 // Cache miss: asynchronously load bitmap to avoid scroll stuttering 200 // in the project picker. 201 new LoadPreviewBitmapTask(this, projectPath, imageView, mItemWidth, mItemHeight, 202 title, duration, mPreviewBitmapCache).execute(); 203 } else { 204 return previewBitmap; 205 } 206 207 return null; 208 } 209 210 private Bitmap renderNewProjectThumbnail() { 211 final Bitmap bitmap = Bitmap.createBitmap(mItemWidth, mItemHeight, 212 Bitmap.Config.ARGB_8888); 213 final Canvas canvas = new Canvas(bitmap); 214 final Paint paint = new Paint(); 215 canvas.drawRect(0, 0, mItemWidth, mItemHeight, paint); 216 217 paint.setTextSize(18.0f); 218 paint.setAlpha(255); 219 final Bitmap newProjectIcon = BitmapFactory.decodeResource(mResources, 220 R.drawable.add_video_project_big); 221 final int x = (mItemWidth - newProjectIcon.getWidth()) / 2; 222 final int y = (mItemHeight - newProjectIcon.getHeight()) / 2; 223 canvas.drawBitmap(newProjectIcon, x, y, paint); 224 newProjectIcon.recycle(); 225 226 return bitmap; 227 } 228 229 /** 230 * Converts milliseconds into the string time format HH:mm:ss. 231 */ 232 private String millisecondsToTimeString(long milliseconds) { 233 return DateUtils.formatElapsedTime(milliseconds / 1000); 234 } 235} 236 237/** 238 * Worker that loads preview bitmap for a project, 239 */ 240class LoadPreviewBitmapTask extends AsyncTask<Void, Void, Bitmap> { 241 // Handle to the adapter that initiates this async task. 242 private ProjectPickerAdapter mContextAdapter; 243 private String mProjectPath; 244 // Handle to the image view we should update when the preview bitmap is loaded. 245 private ImageView mImageView; 246 private int mWidth; 247 private int mHeight; 248 private String mTitle; 249 private String mDuration; 250 private LruCache<String, Bitmap> mPreviewBitmapCache; 251 252 public LoadPreviewBitmapTask(ProjectPickerAdapter contextAdapter, String projectPath, 253 ImageView imageView, int width, int height, String title, String duration, 254 LruCache<String, Bitmap> previewBitmapCache) { 255 mContextAdapter = contextAdapter; 256 mProjectPath = projectPath; 257 mImageView = imageView; 258 mWidth = width; 259 mHeight = height; 260 mTitle = title; 261 mDuration = duration; 262 mPreviewBitmapCache = previewBitmapCache; 263 } 264 265 @Override 266 protected Bitmap doInBackground(Void... param) { 267 final File thumbnail = new File(mProjectPath, VideoEditor.THUMBNAIL_FILENAME); 268 // Return early if thumbnail does not exist. 269 if (!thumbnail.exists()) { 270 return null; 271 } 272 273 try { 274 final Bitmap previewBitmap = ImageUtils.scaleImage( 275 thumbnail.getAbsolutePath(), 276 mWidth, 277 mHeight, 278 ImageUtils.MATCH_LARGER_DIMENSION); 279 if (previewBitmap != null) { 280 final Bitmap bitmap = Bitmap.createBitmap(mWidth, mHeight, 281 Bitmap.Config.ARGB_8888); 282 final Canvas canvas = new Canvas(bitmap); 283 final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); 284 285 // Draw bitmap at the center of the canvas. 286 canvas.drawBitmap(previewBitmap, 287 (mWidth - previewBitmap.getWidth()) / 2, 288 (mHeight - previewBitmap.getHeight()) / 2, 289 paint); 290 return bitmap; 291 } 292 } catch (IOException e) { 293 e.printStackTrace(); 294 } 295 296 return null; 297 } 298 299 @Override 300 protected void onPostExecute(Bitmap result) { 301 if (result == null) { 302 // If we don't have thumbnail, default to a black canvas. 303 result = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888); 304 result.eraseColor(Color.BLACK); 305 } else { 306 mPreviewBitmapCache.put(mProjectPath, result); 307 } 308 309 // Update the image view. 310 mContextAdapter.drawBottomOverlay(result, mTitle, mDuration); 311 mImageView.setImageBitmap(result); 312 } 313} 314