1 /* 2 * Copyright (C) 2019 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 package com.android.wallpaper.picker; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorListenerAdapter; 20 import android.app.Activity; 21 import android.content.Context; 22 import android.graphics.Bitmap; 23 import android.graphics.Bitmap.Config; 24 import android.graphics.Color; 25 import android.graphics.Point; 26 import android.graphics.PointF; 27 import android.graphics.Rect; 28 import android.os.Bundle; 29 import android.view.Display; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.ImageView; 34 35 import androidx.annotation.Nullable; 36 import androidx.fragment.app.FragmentActivity; 37 38 import com.android.wallpaper.R; 39 import com.android.wallpaper.asset.Asset; 40 import com.android.wallpaper.module.WallpaperPersister.Destination; 41 import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback; 42 import com.android.wallpaper.util.ScreenSizeCalculator; 43 import com.android.wallpaper.util.WallpaperCropUtils; 44 45 import com.bumptech.glide.Glide; 46 import com.bumptech.glide.MemoryCategory; 47 import com.davemorrissey.labs.subscaleview.ImageSource; 48 import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; 49 import com.google.android.material.bottomsheet.BottomSheetBehavior; 50 51 /** 52 * Fragment which displays the UI for previewing an individual static wallpaper and its attribution 53 * information. 54 */ 55 public class ImagePreviewFragment extends PreviewFragment { 56 57 private static final float DEFAULT_WALLPAPER_MAX_ZOOM = 8f; 58 59 private SubsamplingScaleImageView mFullResImageView; 60 private Asset mWallpaperAsset; 61 private Point mDefaultCropSurfaceSize; 62 private Point mScreenSize; 63 private Point mRawWallpaperSize; // Native size of wallpaper image. 64 private ImageView mLowResImageView; 65 66 private InfoPageController mInfoPageController; 67 68 @Override onCreate(Bundle savedInstanceState)69 public void onCreate(Bundle savedInstanceState) { 70 super.onCreate(savedInstanceState); 71 mWallpaperAsset = mWallpaper.getAsset(requireContext().getApplicationContext()); 72 } 73 74 @Override getLayoutResId()75 protected int getLayoutResId() { 76 return R.layout.fragment_image_preview; 77 } 78 79 getBottomSheetResId()80 protected int getBottomSheetResId() { 81 return R.id.bottom_sheet; 82 } 83 84 @Override getLoadingIndicatorResId()85 protected int getLoadingIndicatorResId() { 86 return R.id.loading_indicator; 87 } 88 89 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)90 public View onCreateView(LayoutInflater inflater, ViewGroup container, 91 Bundle savedInstanceState) { 92 View view = super.onCreateView(inflater, container, savedInstanceState); 93 94 Activity activity = requireActivity(); 95 96 mFullResImageView = view.findViewById(R.id.full_res_image); 97 98 mInfoPageController = new InfoPageController(view.findViewById(R.id.page_info), 99 mPreviewMode); 100 101 mLowResImageView = view.findViewById(R.id.low_res_image); 102 103 // Trim some memory from Glide to make room for the full-size image in this fragment. 104 Glide.get(activity).setMemoryCategory(MemoryCategory.LOW); 105 106 mDefaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize( 107 getResources(), activity.getWindowManager().getDefaultDisplay()); 108 mScreenSize = ScreenSizeCalculator.getInstance().getScreenSize( 109 activity.getWindowManager().getDefaultDisplay()); 110 111 // Load a low-res placeholder image if there's a thumbnail available from the asset that can 112 // be shown to the user more quickly than the full-sized image. 113 if (mWallpaperAsset.hasLowResDataSource()) { 114 mWallpaperAsset.loadLowResDrawable(activity, mLowResImageView, Color.BLACK, 115 new WallpaperPreviewBitmapTransformation(activity.getApplicationContext(), 116 isRtl())); 117 } 118 119 mWallpaperAsset.decodeRawDimensions(getActivity(), dimensions -> { 120 // Don't continue loading the wallpaper if the Fragment is detached. 121 if (getActivity() == null) { 122 return; 123 } 124 125 // Return early and show a dialog if dimensions are null (signaling a decoding error). 126 if (dimensions == null) { 127 showLoadWallpaperErrorDialog(); 128 return; 129 } 130 131 mRawWallpaperSize = dimensions; 132 setUpExploreIntent(ImagePreviewFragment.this::initFullResView); 133 }); 134 135 setUpLoadingIndicator(); 136 137 return view; 138 } 139 140 @Override setUpBottomSheetView(ViewGroup bottomSheet)141 protected void setUpBottomSheetView(ViewGroup bottomSheet) { 142 // Nothing needed here. 143 } 144 145 @Override isLoaded()146 protected boolean isLoaded() { 147 return mFullResImageView != null && mFullResImageView.hasImage(); 148 } 149 150 @Override onClickOk()151 public void onClickOk() { 152 FragmentActivity activity = getActivity(); 153 if (activity != null) { 154 activity.finish(); 155 } 156 } 157 158 @Override onDestroy()159 public void onDestroy() { 160 super.onDestroy(); 161 if (mLoadingProgressBar != null) { 162 mLoadingProgressBar.hide(); 163 } 164 mFullResImageView.recycle(); 165 } 166 167 @Override onSaveInstanceState(Bundle outState)168 public void onSaveInstanceState(Bundle outState) { 169 super.onSaveInstanceState(outState); 170 171 final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(mBottomSheet); 172 outState.putInt(KEY_BOTTOM_SHEET_STATE, bottomSheetBehavior.getState()); 173 } 174 175 @Override setBottomSheetContentAlpha(float alpha)176 protected void setBottomSheetContentAlpha(float alpha) { 177 mInfoPageController.setContentAlpha(alpha); 178 } 179 180 @Override getExploreButtonLabel(Context context)181 protected CharSequence getExploreButtonLabel(Context context) { 182 return context.getString(mWallpaper.getActionLabelRes(context)); 183 } 184 185 /** 186 * Initializes MosaicView by initializing tiling, setting a fallback page bitmap, and 187 * initializing a zoom-scroll observer and click listener. 188 */ initFullResView()189 private void initFullResView() { 190 mFullResImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP); 191 192 // Set a solid black "page bitmap" so MosaicView draws a black background while waiting 193 // for the image to load or a transparent one if a thumbnail already loaded. 194 Bitmap blackBitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888); 195 int color = (mLowResImageView.getDrawable() == null) ? Color.BLACK : Color.TRANSPARENT; 196 blackBitmap.setPixel(0, 0, color); 197 mFullResImageView.setImage(ImageSource.bitmap(blackBitmap)); 198 199 // Then set a fallback "page bitmap" to cover the whole MosaicView, which is an actual 200 // (lower res) version of the image to be displayed. 201 Point targetPageBitmapSize = new Point(mRawWallpaperSize); 202 mWallpaperAsset.decodeBitmap(targetPageBitmapSize.x, targetPageBitmapSize.y, 203 pageBitmap -> { 204 // Check that the activity is still around since the decoding task started. 205 if (getActivity() == null) { 206 return; 207 } 208 209 // Some of these may be null depending on if the Fragment is paused, stopped, 210 // or destroyed. 211 if (mLoadingProgressBar != null) { 212 mLoadingProgressBar.hide(); 213 } 214 // The page bitmap may be null if there was a decoding error, so show an 215 // error dialog. 216 if (pageBitmap == null) { 217 showLoadWallpaperErrorDialog(); 218 return; 219 } 220 if (mFullResImageView != null) { 221 // Set page bitmap. 222 mFullResImageView.setImage(ImageSource.bitmap(pageBitmap)); 223 224 setDefaultWallpaperZoomAndScroll(); 225 crossFadeInMosaicView(); 226 } 227 getActivity().invalidateOptionsMenu(); 228 229 populateInfoPage(mInfoPageController); 230 }); 231 } 232 233 /** 234 * Makes the MosaicView visible with an alpha fade-in animation while fading out the loading 235 * indicator. 236 */ crossFadeInMosaicView()237 private void crossFadeInMosaicView() { 238 long shortAnimationDuration = getResources().getInteger( 239 android.R.integer.config_shortAnimTime); 240 241 mFullResImageView.setAlpha(0f); 242 mFullResImageView.animate() 243 .alpha(1f) 244 .setDuration(shortAnimationDuration) 245 .setListener(new AnimatorListenerAdapter() { 246 @Override 247 public void onAnimationEnd(Animator animation) { 248 // Clear the thumbnail bitmap reference to save memory since it's no longer 249 // visible. 250 if (mLowResImageView != null) { 251 mLowResImageView.setImageBitmap(null); 252 } 253 } 254 }); 255 256 mLoadingProgressBar.animate() 257 .alpha(0f) 258 .setDuration(shortAnimationDuration) 259 .setListener(new AnimatorListenerAdapter() { 260 @Override 261 public void onAnimationEnd(Animator animation) { 262 if (mLoadingProgressBar != null) { 263 mLoadingProgressBar.hide(); 264 } 265 } 266 }); 267 } 268 269 /** 270 * Sets the default wallpaper zoom and scroll position based on a "crop surface" 271 * (with extra width to account for parallax) superimposed on the screen. Shows as much of the 272 * wallpaper as possible on the crop surface and align screen to crop surface such that the 273 * default preview matches what would be seen by the user in the left-most home screen. 274 * 275 * <p>This method is called once in the Fragment lifecycle after the wallpaper asset has loaded 276 * and rendered to the layout. 277 */ setDefaultWallpaperZoomAndScroll()278 private void setDefaultWallpaperZoomAndScroll() { 279 // Determine minimum zoom to fit maximum visible area of wallpaper on crop surface. 280 float defaultWallpaperZoom = 281 WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mDefaultCropSurfaceSize); 282 float minWallpaperZoom = 283 WallpaperCropUtils.calculateMinZoom(mRawWallpaperSize, mScreenSize); 284 285 Point screenToCropSurfacePosition = WallpaperCropUtils.calculateCenterPosition( 286 mDefaultCropSurfaceSize, mScreenSize, true /* alignStart */, isRtl()); 287 Point zoomedWallpaperSize = new Point( 288 Math.round(mRawWallpaperSize.x * defaultWallpaperZoom), 289 Math.round(mRawWallpaperSize.y * defaultWallpaperZoom)); 290 Point cropSurfaceToWallpaperPosition = WallpaperCropUtils.calculateCenterPosition( 291 zoomedWallpaperSize, mDefaultCropSurfaceSize, false /* alignStart */, isRtl()); 292 293 // Set min wallpaper zoom and max zoom on MosaicView widget. 294 mFullResImageView.setMaxScale(Math.max(DEFAULT_WALLPAPER_MAX_ZOOM, defaultWallpaperZoom)); 295 mFullResImageView.setMinScale(minWallpaperZoom); 296 297 // Set center to composite positioning between scaled wallpaper and screen. 298 PointF centerPosition = new PointF( 299 mRawWallpaperSize.x / 2f, 300 mRawWallpaperSize.y / 2f); 301 centerPosition.offset(-(screenToCropSurfacePosition.x + cropSurfaceToWallpaperPosition.x), 302 -(screenToCropSurfacePosition.y + cropSurfaceToWallpaperPosition.y)); 303 304 mFullResImageView.setScaleAndCenter(minWallpaperZoom, centerPosition); 305 } 306 calculateCropRect()307 private Rect calculateCropRect() { 308 // Calculate Rect of wallpaper in physical pixel terms (i.e., scaled to current zoom). 309 float wallpaperZoom = mFullResImageView.getScale(); 310 int scaledWallpaperWidth = (int) (mRawWallpaperSize.x * wallpaperZoom); 311 int scaledWallpaperHeight = (int) (mRawWallpaperSize.y * wallpaperZoom); 312 Rect rect = new Rect(); 313 mFullResImageView.visibleFileRect(rect); 314 int scrollX = (int) (rect.left * wallpaperZoom); 315 int scrollY = (int) (rect.top * wallpaperZoom); 316 317 rect.set(0, 0, scaledWallpaperWidth, scaledWallpaperHeight); 318 319 Display defaultDisplay = requireActivity().getWindowManager().getDefaultDisplay(); 320 Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay); 321 // Crop rect should start off as the visible screen and then include extra width and height 322 // if available within wallpaper at the current zoom. 323 Rect cropRect = new Rect(scrollX, scrollY, scrollX + screenSize.x, scrollY + screenSize.y); 324 325 Point defaultCropSurfaceSize = WallpaperCropUtils.getDefaultCropSurfaceSize( 326 getResources(), defaultDisplay); 327 int extraWidth = defaultCropSurfaceSize.x - screenSize.x; 328 int extraHeightTopAndBottom = (int) ((defaultCropSurfaceSize.y - screenSize.y) / 2f); 329 330 // Try to increase size of screenRect to include extra width depending on the layout 331 // direction. 332 if (isRtl()) { 333 cropRect.left = Math.max(cropRect.left - extraWidth, rect.left); 334 } else { 335 cropRect.right = Math.min(cropRect.right + extraWidth, rect.right); 336 } 337 338 // Try to increase the size of the cropRect to to include extra height. 339 int availableExtraHeightTop = cropRect.top - Math.max( 340 rect.top, 341 cropRect.top - extraHeightTopAndBottom); 342 int availableExtraHeightBottom = Math.min( 343 rect.bottom, 344 cropRect.bottom + extraHeightTopAndBottom) - cropRect.bottom; 345 346 int availableExtraHeightTopAndBottom = 347 Math.min(availableExtraHeightTop, availableExtraHeightBottom); 348 cropRect.top -= availableExtraHeightTopAndBottom; 349 cropRect.bottom += availableExtraHeightTopAndBottom; 350 351 return cropRect; 352 } 353 354 @Override setCurrentWallpaper(@estination int destination)355 protected void setCurrentWallpaper(@Destination int destination) { 356 mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, mWallpaperAsset, 357 destination, mFullResImageView.getScale(), calculateCropRect(), 358 new SetWallpaperCallback() { 359 @Override 360 public void onSuccess() { 361 finishActivityWithResultOk(); 362 } 363 364 @Override 365 public void onError(@Nullable Throwable throwable) { 366 showSetWallpaperErrorDialog(destination); 367 } 368 }); 369 } 370 } 371