blob: 462095448c55ad534d317ae201fbae055648e6ec [file] [log] [blame]
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -07001/*
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 */
16package com.android.wallpaper.picker;
17
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070018import android.animation.Animator;
19import android.animation.AnimatorListenerAdapter;
20import android.app.Activity;
21import android.content.Context;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070022import android.graphics.Bitmap;
23import android.graphics.Bitmap.Config;
24import android.graphics.Color;
25import android.graphics.Point;
26import android.graphics.PointF;
27import android.graphics.Rect;
28import android.os.Bundle;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070029import android.view.Display;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.view.ViewGroup;
33import android.widget.ImageView;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070034
35import androidx.annotation.Nullable;
36import androidx.fragment.app.FragmentActivity;
37
38import com.android.wallpaper.R;
39import com.android.wallpaper.asset.Asset;
“Chuckffd832c2020-03-22 02:15:58 +080040import com.android.wallpaper.model.WallpaperInfo;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070041import com.android.wallpaper.module.WallpaperPersister.Destination;
42import com.android.wallpaper.module.WallpaperPersister.SetWallpaperCallback;
43import com.android.wallpaper.util.ScreenSizeCalculator;
44import com.android.wallpaper.util.WallpaperCropUtils;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070045
46import com.bumptech.glide.Glide;
47import com.bumptech.glide.MemoryCategory;
48import com.davemorrissey.labs.subscaleview.ImageSource;
49import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
50import com.google.android.material.bottomsheet.BottomSheetBehavior;
51
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070052/**
Santiago Etchebehere4ecb6aa2019-11-07 11:48:58 -080053 * Fragment which displays the UI for previewing an individual static wallpaper and its attribution
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070054 * information.
55 */
56public class ImagePreviewFragment extends PreviewFragment {
57
58 private static final float DEFAULT_WALLPAPER_MAX_ZOOM = 8f;
59
60 private SubsamplingScaleImageView mFullResImageView;
61 private Asset mWallpaperAsset;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070062 private Point mDefaultCropSurfaceSize;
63 private Point mScreenSize;
64 private Point mRawWallpaperSize; // Native size of wallpaper image.
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070065 private ImageView mLowResImageView;
Santiago Etchebeheredc55b192019-11-08 13:56:52 -080066 private InfoPageController mInfoPageController;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070067
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070068 @Override
69 public void onCreate(Bundle savedInstanceState) {
70 super.onCreate(savedInstanceState);
71 mWallpaperAsset = mWallpaper.getAsset(requireContext().getApplicationContext());
72 }
73
74 @Override
75 protected int getLayoutResId() {
Santiago Etchebehere4ecb6aa2019-11-07 11:48:58 -080076 return R.layout.fragment_image_preview;
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070077 }
78
Santiago Etchebeheredc55b192019-11-08 13:56:52 -080079
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070080 protected int getBottomSheetResId() {
81 return R.id.bottom_sheet;
82 }
83
84 @Override
Santiago Etchebeheredc55b192019-11-08 13:56:52 -080085 protected int getLoadingIndicatorResId() {
86 return R.id.loading_indicator;
87 }
88
89 @Override
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070090 public View onCreateView(LayoutInflater inflater, ViewGroup container,
Michel Comin Escudeb3e1fef2019-11-22 11:41:53 -080091 Bundle savedInstanceState) {
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070092 View view = super.onCreateView(inflater, container, savedInstanceState);
93
94 Activity activity = requireActivity();
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070095
96 mFullResImageView = view.findViewById(R.id.full_res_image);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -070097
Santiago Etchebeheredc55b192019-11-08 13:56:52 -080098 mInfoPageController = new InfoPageController(view.findViewById(R.id.page_info),
99 mPreviewMode);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700100
101 mLowResImageView = view.findViewById(R.id.low_res_image);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700102
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
Santiago Etchebeheref4549ed2019-11-13 14:18:57 -0800135 setUpLoadingIndicator();
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700136
137 return view;
138 }
139
Santiago Etchebehere4ecb6aa2019-11-07 11:48:58 -0800140 @Override
141 protected void setUpBottomSheetView(ViewGroup bottomSheet) {
142 // Nothing needed here.
143 }
144
Santiago Etchebeheredc55b192019-11-08 13:56:52 -0800145 @Override
146 protected boolean isLoaded() {
147 return mFullResImageView != null && mFullResImageView.hasImage();
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700148 }
149
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700150 @Override
151 public void onClickOk() {
152 FragmentActivity activity = getActivity();
153 if (activity != null) {
154 activity.finish();
155 }
156 }
157
158 @Override
159 public void onDestroy() {
160 super.onDestroy();
Santiago Etchebeheref4549ed2019-11-13 14:18:57 -0800161 if (mLoadingProgressBar != null) {
162 mLoadingProgressBar.hide();
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700163 }
164 mFullResImageView.recycle();
165 }
166
167 @Override
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
176 protected void setBottomSheetContentAlpha(float alpha) {
Santiago Etchebeheredc55b192019-11-08 13:56:52 -0800177 mInfoPageController.setContentAlpha(alpha);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700178 }
179
Santiago Etchebeheredc55b192019-11-08 13:56:52 -0800180 @Override
181 protected CharSequence getExploreButtonLabel(Context context) {
182 return context.getString(mWallpaper.getActionLabelRes(context));
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700183 }
184
185 /**
186 * Initializes MosaicView by initializing tiling, setting a fallback page bitmap, and
187 * initializing a zoom-scroll observer and click listener.
188 */
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.
Santiago Etchebeheref4549ed2019-11-13 14:18:57 -0800211 if (mLoadingProgressBar != null) {
212 mLoadingProgressBar.hide();
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700213 }
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 }
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700227 getActivity().invalidateOptionsMenu();
228
Santiago Etchebeheredc55b192019-11-08 13:56:52 -0800229 populateInfoPage(mInfoPageController);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700230 });
231 }
232
233 /**
234 * Makes the MosaicView visible with an alpha fade-in animation while fading out the loading
235 * indicator.
236 */
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
Santiago Etchebeheref4549ed2019-11-13 14:18:57 -0800256 mLoadingProgressBar.animate()
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700257 .alpha(0f)
258 .setDuration(shortAnimationDuration)
259 .setListener(new AnimatorListenerAdapter() {
260 @Override
261 public void onAnimationEnd(Animator animation) {
Santiago Etchebeheref4549ed2019-11-13 14:18:57 -0800262 if (mLoadingProgressBar != null) {
263 mLoadingProgressBar.hide();
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700264 }
265 }
266 });
267 }
268
269 /**
Michel Comin Escudeb3e1fef2019-11-22 11:41:53 -0800270 * Sets the default wallpaper zoom and scroll position based on a "crop surface" (with extra
271 * width to account for parallax) superimposed on the screen. Shows as much of the wallpaper as
272 * possible on the crop surface and align screen to crop surface such that the default preview
273 * matches what would be seen by the user in the left-most home screen.
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700274 *
275 * <p>This method is called once in the Fragment lifecycle after the wallpaper asset has loaded
276 * and rendered to the layout.
277 */
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
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700285 // Set min wallpaper zoom and max zoom on MosaicView widget.
286 mFullResImageView.setMaxScale(Math.max(DEFAULT_WALLPAPER_MAX_ZOOM, defaultWallpaperZoom));
287 mFullResImageView.setMinScale(minWallpaperZoom);
288
289 // Set center to composite positioning between scaled wallpaper and screen.
290 PointF centerPosition = new PointF(
291 mRawWallpaperSize.x / 2f,
292 mRawWallpaperSize.y / 2f);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700293
294 mFullResImageView.setScaleAndCenter(minWallpaperZoom, centerPosition);
295 }
296
297 private Rect calculateCropRect() {
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700298 float wallpaperZoom = mFullResImageView.getScale();
Chuck Liao5182a312020-03-04 20:04:18 +0800299 Context context = requireContext().getApplicationContext();
300 Display defaultDisplay = requireActivity().getWindowManager().getDefaultDisplay();
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700301 Rect rect = new Rect();
302 mFullResImageView.visibleFileRect(rect);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700303
Chuck Liao5182a312020-03-04 20:04:18 +0800304 return WallpaperCropUtils.calculateCropRect(context, defaultDisplay, mRawWallpaperSize,
305 rect, wallpaperZoom);
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700306 }
307
308 @Override
309 protected void setCurrentWallpaper(@Destination int destination) {
310 mWallpaperSetter.setCurrentWallpaper(getActivity(), mWallpaper, mWallpaperAsset,
311 destination, mFullResImageView.getScale(), calculateCropRect(),
312 new SetWallpaperCallback() {
313 @Override
“Chuckffd832c2020-03-22 02:15:58 +0800314 public void onSuccess(WallpaperInfo wallpaperInfo) {
Santiago Etchebehere6d3d1e62019-09-26 11:11:11 -0700315 finishActivityWithResultOk();
316 }
317
318 @Override
319 public void onError(@Nullable Throwable throwable) {
320 showSetWallpaperErrorDialog(destination);
321 }
322 });
323 }
324}