From 8ab609f84f9c73c9cdacf5f5e88a1db12ae343c8 Mon Sep 17 00:00:00 2001 From: wintmain Date: Mon, 6 Oct 2025 10:44:33 +0800 Subject: [PATCH 1/3] [common][refactor]Adjust res project --- app-catalog/androidres/.gitignore | 7 +++++++ app-catalog/{samples => }/androidres/README.md | 2 +- app-catalog/{samples => }/androidres/build.gradle | 0 .../{samples => }/androidres/src/main/res/placeholder | 0 app-catalog/samples/androidres/src/main/.gitignore | 3 --- settings.gradle | 1 + 6 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 app-catalog/androidres/.gitignore rename app-catalog/{samples => }/androidres/README.md (89%) rename app-catalog/{samples => }/androidres/build.gradle (100%) rename app-catalog/{samples => }/androidres/src/main/res/placeholder (100%) delete mode 100644 app-catalog/samples/androidres/src/main/.gitignore diff --git a/app-catalog/androidres/.gitignore b/app-catalog/androidres/.gitignore new file mode 100644 index 0000000..06a2b8e --- /dev/null +++ b/app-catalog/androidres/.gitignore @@ -0,0 +1,7 @@ +#Exclude everything +* + +!README.md +!.gitignore +!build.gradle +!src/main/res/placeholder diff --git a/app-catalog/samples/androidres/README.md b/app-catalog/androidres/README.md similarity index 89% rename from app-catalog/samples/androidres/README.md rename to app-catalog/androidres/README.md index 75c7109..a3e88e5 100644 --- a/app-catalog/samples/androidres/README.md +++ b/app-catalog/androidres/README.md @@ -5,4 +5,4 @@ > 本地带着复制来的 res 参与编译可能会失败,删除即可。 -[回到主页](../../../README.md) +[回到主页](../../README.md) diff --git a/app-catalog/samples/androidres/build.gradle b/app-catalog/androidres/build.gradle similarity index 100% rename from app-catalog/samples/androidres/build.gradle rename to app-catalog/androidres/build.gradle diff --git a/app-catalog/samples/androidres/src/main/res/placeholder b/app-catalog/androidres/src/main/res/placeholder similarity index 100% rename from app-catalog/samples/androidres/src/main/res/placeholder rename to app-catalog/androidres/src/main/res/placeholder diff --git a/app-catalog/samples/androidres/src/main/.gitignore b/app-catalog/samples/androidres/src/main/.gitignore deleted file mode 100644 index 607f973..0000000 --- a/app-catalog/samples/androidres/src/main/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -**/** -!res/placeholder -!.gitignore diff --git a/settings.gradle b/settings.gradle index 347bd6f..30ded5d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,7 @@ import java.util.regex.Matcher */ include ':app-catalog:app' +include ':app-catalog:androidres' // Dynamically include samples under /app-catalog/samples/ folder def samples = [] // Find all build.gradle files under samples folder From 0f15b6336243e3e383b2b42f78650fabf92f9baf Mon Sep 17 00:00:00 2001 From: wintmain Date: Mon, 6 Oct 2025 10:48:32 +0800 Subject: [PATCH 2/3] [wBasis][feat]Add activity scene transition sample --- app-catalog/app/proguard-rules.pro | 4 + app-catalog/samples/wBasis/build.gradle | 5 +- .../wBasis/src/main/AndroidManifest.xml | 10 ++ .../SceneTransitionBasicDetailActivity.java | 164 ++++++++++++++++++ .../SceneTransitionBasicMainActivity.java | 142 +++++++++++++++ .../util/SceneTransitionBasicItem.java | 76 ++++++++ .../transition/util/SquareFrameLayout.java | 69 ++++++++ ...ctivity_scene_transition_basic_details.xml | 57 ++++++ .../activity_scene_transition_basic_grid.xml | 26 +++ ...ivity_scene_transition_basic_grid_item.xml | 45 +++++ .../transition/grid_detail_transition.xml | 29 ++++ .../src/main/res-transition/values/dimens.xml | 2 + .../main/res-transition/values/strings.xml | 38 ++++ .../src/main/res-transition/values/styles.xml | 32 ++++ 14 files changed, 696 insertions(+), 3 deletions(-) create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicDetailActivity.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicMainActivity.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SceneTransitionBasicItem.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SquareFrameLayout.java create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_details.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid_item.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/transition/grid_detail_transition.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml diff --git a/app-catalog/app/proguard-rules.pro b/app-catalog/app/proguard-rules.pro index 2adf2a4..55ccbde 100644 --- a/app-catalog/app/proguard-rules.pro +++ b/app-catalog/app/proguard-rules.pro @@ -49,3 +49,7 @@ -dontwarn org.joda.time.format.DateTimeFormat -dontwarn org.joda.time.format.DateTimeFormatter -dontwarn springfox.documentation.spring.web.json.Json + +-dontwarn com.squareup.okhttp.Cache +-dontwarn com.squareup.okhttp.OkHttpClient +-dontwarn com.squareup.okhttp.OkUrlFactory diff --git a/app-catalog/samples/wBasis/build.gradle b/app-catalog/samples/wBasis/build.gradle index 2a1e011..faadc67 100644 --- a/app-catalog/samples/wBasis/build.gradle +++ b/app-catalog/samples/wBasis/build.gradle @@ -55,7 +55,6 @@ dependencies { implementation libs.androidx.lifecycle.runtime.ktx implementation libs.androidx.ui.graphics implementation libs.compose.ui.tooling.preview - androidTestImplementation libs.compose.ui.test.junit4 - debugImplementation libs.compose.ui.tooling - debugImplementation libs.compose.ui.test.manifest + + implementation 'com.squareup.picasso:picasso:2.4.0' } \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/AndroidManifest.xml b/app-catalog/samples/wBasis/src/main/AndroidManifest.xml index 883c0c2..7a1a701 100644 --- a/app-catalog/samples/wBasis/src/main/AndroidManifest.xml +++ b/app-catalog/samples/wBasis/src/main/AndroidManifest.xml @@ -377,6 +377,16 @@ android:name=".transition.CustomTransitionActivity" android:exported="true" android:theme="@style/Theme.AppCompat.DayNight" /> + + + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicDetailActivity.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicDetailActivity.java new file mode 100644 index 0000000..9613376 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicDetailActivity.java @@ -0,0 +1,164 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition; + +import android.os.Build; +import android.os.Bundle; +import android.transition.Transition; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.ViewCompat; +import com.squareup.picasso.Picasso; +import com.wintmain.wBasis.R; +import com.wintmain.wBasis.transition.util.SceneTransitionBasicItem; + +/** + * Our secondary Activity which is launched from {@link SceneTransitionBasicMainActivity}. + * Has a simple detail UI which has a large banner image, title and body text. + */ +public class SceneTransitionBasicDetailActivity extends AppCompatActivity { + + // Extra name for the ID parameter + public static final String EXTRA_PARAM_ID = "detail:_id"; + + // View name of the header image. Used for activity scene transitions + public static final String VIEW_NAME_HEADER_IMAGE = "detail:header:image"; + + // View name of the header title. Used for activity scene transitions + public static final String VIEW_NAME_HEADER_TITLE = "detail:header:title"; + + private ImageView mHeaderImageView; + private TextView mHeaderTitle; + + private SceneTransitionBasicItem mSceneTransitionBasicItem; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_scene_transition_basic_details); + + // Retrieve the correct Item instance, using the ID provided in the Intent + mSceneTransitionBasicItem = SceneTransitionBasicItem.getItem( + getIntent().getIntExtra(EXTRA_PARAM_ID, 0)); + + mHeaderImageView = findViewById(R.id.imageview_header); + mHeaderTitle = findViewById(R.id.textview_title); + + // BEGIN_INCLUDE(detail_set_view_name) + /* + * Set the name of the view's which will be transition to, using the static values above. + * This could be done in the layout XML, but exposing it via static variables allows easy + * querying from other Activities + */ + ViewCompat.setTransitionName(mHeaderImageView, VIEW_NAME_HEADER_IMAGE); + ViewCompat.setTransitionName(mHeaderTitle, VIEW_NAME_HEADER_TITLE); + // END_INCLUDE(detail_set_view_name) + + loadItem(); + } + + private void loadItem() { + // Set the title TextView to the item's name and author + mHeaderTitle.setText(getString(R.string.image_header, mSceneTransitionBasicItem.getName(), + mSceneTransitionBasicItem.getAuthor())); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && addTransitionListener()) { + // If we're running on Lollipop and we have added a listener to the shared element + // transition, load the thumbnail. The listener will load the full-size image when + // the transition is complete. + loadThumbnail(); + } else { + // If all other cases we should just load the full-size image now + loadFullSizeImage(); + } + } + + /** + * Load the item's thumbnail image into our {@link ImageView}. + */ + private void loadThumbnail() { + Picasso.with(mHeaderImageView.getContext()) + .load(mSceneTransitionBasicItem.getThumbnailUrl()) + .noFade() + .into(mHeaderImageView); + } + + /** + * Load the item's full-size image into our {@link ImageView}. + */ + private void loadFullSizeImage() { + Picasso.with(mHeaderImageView.getContext()) + .load(mSceneTransitionBasicItem.getPhotoUrl()) + .noFade() + .noPlaceholder() + .into(mHeaderImageView); + } + + /** + * Try and add a {@link Transition.TransitionListener} to the entering shared element + * {@link Transition}. We do this so that we can load the full-size image after the transition + * has completed. + * + * @return true if we were successful in adding a listener to the enter transition + */ + @RequiresApi(21) + private boolean addTransitionListener() { + final Transition transition = getWindow().getSharedElementEnterTransition(); + + if (transition != null) { + // There is an entering shared element transition so add a listener to it + transition.addListener(new Transition.TransitionListener() { + @Override + public void onTransitionEnd(Transition transition) { + // As the transition has ended, we can now load the full-size image + loadFullSizeImage(); + + // Make sure we remove ourselves as a listener + transition.removeListener(this); + } + + @Override + public void onTransitionStart(Transition transition) { + // No-op + } + + @Override + public void onTransitionCancel(Transition transition) { + // Make sure we remove ourselves as a listener + transition.removeListener(this); + } + + @Override + public void onTransitionPause(Transition transition) { + // No-op + } + + @Override + public void onTransitionResume(Transition transition) { + // No-op + } + }); + return true; + } + + // If we reach here then we have not added a listener + return false; + } + +} \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicMainActivity.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicMainActivity.java new file mode 100644 index 0000000..e08815b --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/SceneTransitionBasicMainActivity.java @@ -0,0 +1,142 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.*; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.app.ActivityOptionsCompat; +import androidx.core.util.Pair; +import com.google.android.catalog.framework.annotations.Sample; +import com.squareup.picasso.Picasso; +import com.wintmain.wBasis.R; +import com.wintmain.wBasis.transition.util.SceneTransitionBasicItem; + +/** + * Displays a grid of items which an image and title. + * When the user clicks on an item, {@link SceneTransitionBasicDetailActivity} is launched, + * using the Activity Scene Transitions framework to animatedly do so. + */ +@Sample(name = "ActivitySceneTransitionBaic", + description = "Activity Scene的变化", + tags = {"android-samples", "animation-samples"} +) +public class SceneTransitionBasicMainActivity extends AppCompatActivity { + + private final AdapterView.OnItemClickListener mOnItemClickListener + = new AdapterView.OnItemClickListener() { + + /** + * Called when an item in the {@link android.widget.GridView} is clicked. Here will launch + * the {@link SceneTransitionBasicDetailActivity}, using the Scene Transition animation + * functionality. + */ + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long id) { + SceneTransitionBasicItem sceneTransitionBasicItem = + (SceneTransitionBasicItem) adapterView.getItemAtPosition(position); + + // Construct an Intent as normal + Intent intent = new Intent(SceneTransitionBasicMainActivity.this, + SceneTransitionBasicDetailActivity.class); + intent.putExtra(SceneTransitionBasicDetailActivity.EXTRA_PARAM_ID, + sceneTransitionBasicItem.getId()); + + // BEGIN_INCLUDE(start_activity) + /* + * Now create an {@link android.app.ActivityOptions} instance using the + * {@link ActivityOptionsCompat#makeSceneTransitionAnimation(Activity, Pair[])} factory + * method. + */ + @SuppressWarnings("unchecked") + ActivityOptionsCompat activityOptions = + ActivityOptionsCompat.makeSceneTransitionAnimation( + SceneTransitionBasicMainActivity.this, + + // Now we provide a list of Pair items which contain the view we can + // transitioning from, and the name of the view it is transitioning to, + // in the launched activity + new Pair<>(view.findViewById(R.id.imageview_item), + SceneTransitionBasicDetailActivity.VIEW_NAME_HEADER_IMAGE), + new Pair<>(view.findViewById(R.id.textview_name), + SceneTransitionBasicDetailActivity.VIEW_NAME_HEADER_TITLE)); + + // Now we can start the Activity, providing the activity options as a bundle + ActivityCompat.startActivity(SceneTransitionBasicMainActivity.this, intent, + activityOptions.toBundle()); + // END_INCLUDE(start_activity) + } + }; + + /** + * {@link android.widget.BaseAdapter} which displays items. + */ + private class GridAdapter extends BaseAdapter { + + @Override + public int getCount() { + return SceneTransitionBasicItem.SceneTransitionBasicITEMS.length; + } + + @Override + public SceneTransitionBasicItem getItem(int position) { + return SceneTransitionBasicItem.SceneTransitionBasicITEMS[position]; + } + + @Override + public long getItemId(int position) { + return getItem(position).getId(); + } + + @Override + public View getView(int position, View view, ViewGroup viewGroup) { + if (view == null) { + view = getLayoutInflater().inflate(R.layout.activity_scene_transition_basic_grid_item, viewGroup, false); + } + + final SceneTransitionBasicItem item = getItem(position); + + // Load the thumbnail image + ImageView image = (ImageView) view.findViewById(R.id.imageview_item); + Picasso.with(image.getContext()).load(item.getThumbnailUrl()).into(image); + + // Set the TextView's contents + TextView name = (TextView) view.findViewById(R.id.textview_name); + name.setText(item.getName()); + + return view; + } + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_scene_transition_basic_grid); + + GridView gridView = findViewById(R.id.grid); + gridView.setOnItemClickListener(mOnItemClickListener); + + // 没有adapter,页面是空白的 + GridAdapter mAdapter = new GridAdapter(); + gridView.setAdapter(mAdapter); + } +} diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SceneTransitionBasicItem.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SceneTransitionBasicItem.java new file mode 100644 index 0000000..ff8a83f --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SceneTransitionBasicItem.java @@ -0,0 +1,76 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition.util; + +/** + * Represents an Item in our application. Each item has a name, id, full size image url and + * thumbnail url. + */ +public class SceneTransitionBasicItem { + + private static final String LARGE_BASE_URL = "https://picsum.photos/600/400?image="; + private static final String THUMB_BASE_URL = "https://picsum.photos/100/100?image="; + + public static SceneTransitionBasicItem[] SceneTransitionBasicITEMS = new SceneTransitionBasicItem[]{ + new SceneTransitionBasicItem("Flying in the Light", "Romain Guy", "1"), + new SceneTransitionBasicItem("Caterpillar", "Romain Guy", "10"), + new SceneTransitionBasicItem("Look Me in the Eye", "Romain Guy", "100"), + new SceneTransitionBasicItem("Flamingo", "Romain Guy", "1000"), + new SceneTransitionBasicItem("Rainbow", "Romain Guy", "1001"), + new SceneTransitionBasicItem("Over there", "Romain Guy", "1002"), + new SceneTransitionBasicItem("Jelly Fish 2", "Romain Guy", "1003"), + new SceneTransitionBasicItem("Lone Pine Sunset", "Romain Guy", "1004"), + }; + private final String mName; + private final String mAuthor; + private final String mFileName; + SceneTransitionBasicItem(String name, String author, String fileName) { + mName = name; + mAuthor = author; + mFileName = fileName; + } + + public static SceneTransitionBasicItem getItem(int id) { + for (SceneTransitionBasicItem sceneTransitionBasicItem : SceneTransitionBasicITEMS) { + if (sceneTransitionBasicItem.getId() == id) { + return sceneTransitionBasicItem; + } + } + return null; + } + + public int getId() { + return mName.hashCode() + mFileName.hashCode(); + } + + public String getAuthor() { + return mAuthor; + } + + public String getName() { + return mName; + } + + public String getPhotoUrl() { + return LARGE_BASE_URL + mFileName; + } + + public String getThumbnailUrl() { + return THUMB_BASE_URL + mFileName; + } + +} diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SquareFrameLayout.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SquareFrameLayout.java new file mode 100644 index 0000000..efb1980 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/SquareFrameLayout.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition.util; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.FrameLayout; + +/** + * {@link android.widget.FrameLayout} which forces itself to be laid out as square. + */ +public class SquareFrameLayout extends FrameLayout { + + public SquareFrameLayout(Context context) { + this(context, null); + } + + public SquareFrameLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SquareFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthSize == 0 && heightSize == 0) { + // If there are no constraints on size, let FrameLayout measure + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + // Now use the smallest of the measured dimensions for both dimensions + final int minSize = Math.min(getMeasuredWidth(), getMeasuredHeight()); + setMeasuredDimension(minSize, minSize); + return; + } + + final int size; + if (widthSize == 0 || heightSize == 0) { + // If one of the dimensions has no restriction on size, set both dimensions to be the + // on that does + size = Math.max(widthSize, heightSize); + } else { + // Both dimensions have restrictions on size, set both dimensions to be the + // smallest of the two + size = Math.min(widthSize, heightSize); + } + + final int newMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); + super.onMeasure(newMeasureSpec, newMeasureSpec); + } +} diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_details.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_details.xml new file mode 100644 index 0000000..bc89006 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_details.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid.xml new file mode 100644 index 0000000..623c145 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid.xml @@ -0,0 +1,26 @@ + + + diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid_item.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid_item.xml new file mode 100644 index 0000000..98d55d8 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_scene_transition_basic_grid_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + diff --git a/app-catalog/samples/wBasis/src/main/res-transition/transition/grid_detail_transition.xml b/app-catalog/samples/wBasis/src/main/res-transition/transition/grid_detail_transition.xml new file mode 100644 index 0000000..5e0d0ed --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/transition/grid_detail_transition.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml b/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml index 168aa8d..093d36b 100644 --- a/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml +++ b/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml @@ -32,4 +32,6 @@ @dimen/margin_medium @dimen/margin_medium + + 4dp diff --git a/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml b/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml index 1765a8b..0a33a27 100644 --- a/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml +++ b/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml @@ -46,4 +46,42 @@ Show Log Hide Log + + + + + + Bacon ipsum dolor sit amet venison shankle pork chop meatball tri-tip beef ribs turducken. + Strip steak ball tip boudin shank, turducken leberkas pork chop beef ribs ham hock sausage + frankfurter prosciutto doner ham. Bacon landjaeger ball tip, andouille chuck beef ribs jowl + kevin tri-tip turkey biltong frankfurter sausage. Boudin pork belly meatloaf chicken cow + tri-tip kielbasa shoulder. Pork loin pig boudin hamburger pastrami short ribs. Sirloin + tongue pork loin chicken spare ribs bresaola pastrami.\n\n + Tenderloin turducken pork chop, pork belly beef ribs brisket ham. Turducken landjaeger short + loin capicola pancetta pork chop strip steak rump meatloaf brisket kevin doner short ribs + salami. Beef prosciutto flank leberkas landjaeger swine. Fatback prosciutto sausage, jerky + tail tongue hamburger jowl biltong shank pork belly swine filet mignon chicken ground round. + Pork chop porchetta ground round tri-tip tail t-bone.\n\n + Landjaeger rump bacon salami sausage pork loin pig brisket strip steak corned beef. Biltong + sirloin meatloaf ribeye, bresaola cow chicken t-bone frankfurter andouille strip steak jerky + capicola. Ribeye porchetta strip steak boudin. Kielbasa cow brisket pastrami ball tip + tenderloin bresaola ham. Biltong andouille chuck ham hock jerky beef chicken, flank shankle + ball tip venison porchetta kevin fatback kielbasa. Boudin tongue ground round, turkey pork + belly salami corned beef pork tri-tip meatloaf sausage andouille strip steak pig. Spare ribs + beef meatloaf rump sausage doner frankfurter.\n\n + Fatback porchetta pork loin meatball, turducken pork chop drumstick boudin. Kevin tri-tip + ground round corned beef, ribeye swine filet mignon salami pork spare ribs pork chop + brisket. Filet mignon shankle t-bone bacon. Turducken capicola turkey porchetta kielbasa + shank pork loin jerky venison tenderloin boudin ham hock ground round. + + %1$s by %2$s diff --git a/app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml b/app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml new file mode 100644 index 0000000..7d4aab5 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file From d1119b1df8fdbe26db446f3c9343000e72777788 Mon Sep 17 00:00:00 2001 From: wintmain Date: Wed, 8 Oct 2025 18:41:05 +0800 Subject: [PATCH 3/3] [wBasis][feat]Add grid to pager sample --- app-catalog/samples/wBasis/build.gradle | 1 + .../wBasis/src/main/AndroidManifest.xml | 26 ++- .../transition/GridToPagerFragment.java | 125 +++++++++++ .../transition/GridToPagerMainActivity.java | 69 ++++++ .../transition/adapter/GridAdapter.java | 208 ++++++++++++++++++ .../transition/adapter/ImagePagerAdapter.java | 46 ++++ .../transition/fragment/ImageFragment.java | 98 +++++++++ .../fragment/ImagePagerFragment.java | 103 +++++++++ .../wBasis/transition/util/ImageData.java | 53 +++++ .../layout/activity_grid_to_pager_main.xml | 20 ++ .../layout/fragment_grid_image_view.xml | 22 ++ .../layout/fragment_grid_pager_view.xml | 20 ++ .../layout/fragment_grid_to_pager.xml | 27 +++ .../res-transition/layout/view_image_card.xml | 39 ++++ .../transition/grid_exit_transition.xml | 24 ++ .../image_shared_element_transition.xml | 24 ++ .../src/main/res-transition/values/colors.xml | 21 ++ .../src/main/res-transition/values/dimens.xml | 1 + .../main/res-transition/values/strings.xml | 1 + .../src/main/res-transition/values/styles.xml | 8 + 20 files changed, 925 insertions(+), 11 deletions(-) create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerFragment.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerMainActivity.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/GridAdapter.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/ImagePagerAdapter.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImageFragment.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImagePagerFragment.java create mode 100644 app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/ImageData.java create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/activity_grid_to_pager_main.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_image_view.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_pager_view.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_to_pager.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/layout/view_image_card.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/transition/grid_exit_transition.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/transition/image_shared_element_transition.xml create mode 100644 app-catalog/samples/wBasis/src/main/res-transition/values/colors.xml diff --git a/app-catalog/samples/wBasis/build.gradle b/app-catalog/samples/wBasis/build.gradle index faadc67..b22e42b 100644 --- a/app-catalog/samples/wBasis/build.gradle +++ b/app-catalog/samples/wBasis/build.gradle @@ -55,6 +55,7 @@ dependencies { implementation libs.androidx.lifecycle.runtime.ktx implementation libs.androidx.ui.graphics implementation libs.compose.ui.tooling.preview + implementation libs.glide implementation 'com.squareup.picasso:picasso:2.4.0' } \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/AndroidManifest.xml b/app-catalog/samples/wBasis/src/main/AndroidManifest.xml index 7a1a701..20f1063 100644 --- a/app-catalog/samples/wBasis/src/main/AndroidManifest.xml +++ b/app-catalog/samples/wBasis/src/main/AndroidManifest.xml @@ -37,7 +37,7 @@ android:label="title_activity_main2" android:theme="@style/Theme.Android_Kickoff" /> - + - + - + - + - + - + - + - + android:theme="@style/Theme.AppCompat.DayNight" /> + - - @@ -356,6 +354,7 @@ android:name=".aidl.PeopleRemoteService" android:enabled="true" android:exported="true" + android:permission="TODO" android:process=":remote" /> + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerFragment.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerFragment.java new file mode 100644 index 0000000..506b539 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerFragment.java @@ -0,0 +1,125 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition; + +import android.os.Bundle; +import android.transition.TransitionInflater; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnLayoutChangeListener; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.SharedElementCallback; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; +import com.wintmain.wBasis.R; +import com.wintmain.wBasis.transition.adapter.GridAdapter; + +import java.util.List; +import java.util.Map; + +/** + * A fragment for displaying a grid of images. + */ +public class GridToPagerFragment extends Fragment { + + private RecyclerView recyclerView; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + recyclerView = (RecyclerView) inflater.inflate(R.layout.fragment_grid_to_pager, container, + false); + recyclerView.setAdapter(new GridAdapter(this)); + + prepareTransitions(); + postponeEnterTransition(); + + return recyclerView; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + scrollToPosition(); + } + + /** + * Scrolls the recycler view to show the last viewed item in the grid. This is important when + * navigating back from the grid. + */ + private void scrollToPosition() { + recyclerView.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + recyclerView.removeOnLayoutChangeListener(this); + final RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager(); + View viewAtPosition = layoutManager.findViewByPosition( + GridToPagerMainActivity.currentPosition); + // Scroll to position if the view for the current position is null (not currently + // part of + // layout manager children), or it's not completely visible. + if (viewAtPosition == null || layoutManager + .isViewPartiallyVisible(viewAtPosition, false, true)) { + recyclerView.post(() -> layoutManager.scrollToPosition( + GridToPagerMainActivity.currentPosition)); + } + } + }); + } + + /** + * Prepares the shared element transition to the pager fragment, as well as the other + * transitions + * that affect the flow. + */ + private void prepareTransitions() { + setExitTransition(TransitionInflater.from(getContext()) + .inflateTransition(R.transition.grid_exit_transition)); + + // A similar mapping is set at the ImagePagerFragment with a setEnterSharedElementCallback. + setExitSharedElementCallback( + new SharedElementCallback() { + @Override + public void onMapSharedElements(List names, + Map sharedElements) { + // Locate the ViewHolder for the clicked position. + RecyclerView.ViewHolder selectedViewHolder = recyclerView + .findViewHolderForAdapterPosition( + GridToPagerMainActivity.currentPosition); + if (selectedViewHolder == null) { + return; + } + + // Map the first shared element name to the child ImageView. + sharedElements + .put(names.get(0), + selectedViewHolder.itemView.findViewById(R.id.card_image)); + } + }); + } +} \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerMainActivity.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerMainActivity.java new file mode 100644 index 0000000..57928f8 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/GridToPagerMainActivity.java @@ -0,0 +1,69 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentManager; +import com.google.android.catalog.framework.annotations.Sample; +import com.wintmain.wBasis.R; + +/** + * Grid to pager app's main activity. + */ +@Sample(name = "Grid to pager", + description = "viewpager & glide 照片展示", + tags = {"android-samples", "animation-samples"} +) +public class GridToPagerMainActivity extends AppCompatActivity { + + private static final String KEY_CURRENT_POSITION = + "com.wintmain.wBasis.gridtopager.key.currentPosition"; + /** + * Holds the current image position to be shared between the grid and the pager fragments. This + * position updated when a grid item is clicked, or when paging the pager. + * + * In this demo app, the position always points to an image index at the {@link + * com.wintmain.wBasis.transition.util.ImageData} class. + */ + public static int currentPosition; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_grid_to_pager_main); + if (savedInstanceState != null) { + currentPosition = savedInstanceState.getInt(KEY_CURRENT_POSITION, 0); + // Return here to prevent adding additional GridFragments when changing orientation. + return; + } + FragmentManager fragmentManager = getSupportFragmentManager(); + fragmentManager + .beginTransaction() + .add(R.id.fragment_container, new GridToPagerFragment(), + GridToPagerFragment.class.getSimpleName()) + .commit(); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(KEY_CURRENT_POSITION, currentPosition); + } + +} diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/GridAdapter.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/GridAdapter.java new file mode 100644 index 0000000..bd22bdb --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/GridAdapter.java @@ -0,0 +1,208 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition.adapter; + +import android.graphics.drawable.Drawable; +import android.transition.TransitionSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestManager; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.wintmain.wBasis.R; +import com.wintmain.wBasis.transition.GridToPagerMainActivity; +import com.wintmain.wBasis.transition.fragment.ImagePagerFragment; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static com.wintmain.wBasis.transition.util.ImageData.IMAGE_DRAWABLES; + +/** + * A fragment for displaying a grid of images. + */ +public class GridAdapter extends RecyclerView.Adapter { + private final RequestManager requestManager; + private final ViewHolderListener viewHolderListener; + + /** + * Constructs a new grid adapter for the given {@link Fragment}. + */ + public GridAdapter(Fragment fragment) { + this.requestManager = Glide.with(fragment); + this.viewHolderListener = new ViewHolderListenerImpl(fragment); + } + + @NonNull + @Override + public ImageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.view_image_card, parent, false); + return new ImageViewHolder(view, requestManager, viewHolderListener); + } + + @Override + public void onBindViewHolder(@NonNull ImageViewHolder holder, int position) { + holder.onBind(); + } + + @Override + public int getItemCount() { + return IMAGE_DRAWABLES.length; + } + + /** + * A listener that is attached to all ViewHolders to handle image loading events and clicks. + */ + private interface ViewHolderListener { + + void onLoadCompleted(ImageView view, int adapterPosition); + + void onItemClicked(View view, int adapterPosition); + } + + /** + * Default {@link ViewHolderListener} implementation. + */ + private static class ViewHolderListenerImpl implements ViewHolderListener { + + private Fragment fragment; + private AtomicBoolean enterTransitionStarted; + + ViewHolderListenerImpl(Fragment fragment) { + this.fragment = fragment; + this.enterTransitionStarted = new AtomicBoolean(); + } + + @Override + public void onLoadCompleted(ImageView view, int position) { + // Call startPostponedEnterTransition only when the 'selected' image loading is + // completed. + if (GridToPagerMainActivity.currentPosition != position) { + return; + } + if (enterTransitionStarted.getAndSet(true)) { + return; + } + fragment.startPostponedEnterTransition(); + } + + /** + * Handles a view click by setting the current position to the given {@code position} and + * starting a {@link ImagePagerFragment} which displays the image at the position. + * + * @param view the clicked {@link ImageView} (the shared element view will be + * re-mapped at the + * GridFragment's SharedElementCallback) + * @param position the selected view position + */ + @Override + public void onItemClicked(View view, int position) { + // Update the position. + GridToPagerMainActivity.currentPosition = position; + + // Exclude the clicked card from the exit transition (e.g. the card will disappear + // immediately + // instead of fading out with the rest to prevent an overlapping animation of fade + // and move). + ((TransitionSet) fragment.getExitTransition()).excludeTarget(view, true); + + ImageView transitioningView = view.findViewById(R.id.card_image); + fragment.getFragmentManager() + .beginTransaction() + .setReorderingAllowed(true) // Optimize for shared element transition + .addSharedElement(transitioningView, transitioningView.getTransitionName()) + .replace(R.id.fragment_container, new ImagePagerFragment(), + ImagePagerFragment.class.getSimpleName()) + .addToBackStack(null) + .commit(); + } + } + + /** + * ViewHolder for the grid's images. + */ + static class ImageViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { + + private final ImageView image; + private final RequestManager requestManager; + private final ViewHolderListener viewHolderListener; + + public ImageViewHolder(@NonNull View itemView, RequestManager requestManager, + ViewHolderListener viewHolderListener) { + super(itemView); + + this.image = itemView.findViewById(R.id.card_image); + this.requestManager = requestManager; + this.viewHolderListener = viewHolderListener; + itemView.findViewById(R.id.card_view).setOnClickListener(this); + } + + /** + * Binds this view holder to the given adapter position. + * + * The binding will load the image into the image view, as well as set its transition + * name for + * later. + */ + void onBind() { + int adapterPosition = getAdapterPosition(); + setImage(adapterPosition); + // Set the string value of the image resource as the unique transition name for the + // view. + image.setTransitionName(String.valueOf(IMAGE_DRAWABLES[adapterPosition])); + } + + void setImage(final int adapterPosition) { + // Load the image with Glide to prevent OOM error when the image drawables are very + // large. + requestManager + .load(IMAGE_DRAWABLES[adapterPosition]) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, + Target target, boolean isFirstResource) { + viewHolderListener.onLoadCompleted(image, adapterPosition); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, + Target + target, DataSource dataSource, boolean isFirstResource) { + viewHolderListener.onLoadCompleted(image, adapterPosition); + return false; + } + }).into(image); + } + + + @Override + public void onClick(View v) { + // Let the listener start the ImagePagerFragment. + viewHolderListener.onItemClicked(v, getAdapterPosition()); + } + } +} diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/ImagePagerAdapter.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/ImagePagerAdapter.java new file mode 100644 index 0000000..6a97bce --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/adapter/ImagePagerAdapter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition.adapter; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentStatePagerAdapter; +import com.wintmain.wBasis.transition.fragment.ImageFragment; + +import static com.wintmain.wBasis.transition.util.ImageData.IMAGE_DRAWABLES; + +public class ImagePagerAdapter extends FragmentStatePagerAdapter { + + // public ImagePagerAdapter(@NonNull FragmentManager fm) { +// super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); +// } + public ImagePagerAdapter(Fragment fragment) { + // Note: Initialize with the child fragment manager. + super(fragment.getChildFragmentManager(), BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @NonNull + @Override + public Fragment getItem(int position) { + return ImageFragment.newInstance(IMAGE_DRAWABLES[position]); + } + + @Override + public int getCount() { + return IMAGE_DRAWABLES.length; + } +} diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImageFragment.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImageFragment.java new file mode 100644 index 0000000..65d937a --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImageFragment.java @@ -0,0 +1,98 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition.fragment; + +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; +import com.wintmain.wBasis.R; + +/** + * A fragment for displaying an image. + */ +public class ImageFragment extends Fragment { + private static final String KEY_IMAGE_RES = "com.wintmain.wBasis.gridtopager.key.imageRes"; + + public static ImageFragment newInstance(@DrawableRes int drawableRes) { + ImageFragment fragment = new ImageFragment(); + Bundle argument = new Bundle(); + argument.putInt(KEY_IMAGE_RES, drawableRes); + fragment.setArguments(argument); + return fragment; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final View view = inflater.inflate(R.layout.fragment_grid_image_view, container, false); + Bundle arguments = getArguments(); + @DrawableRes int imageRes = arguments.getInt(KEY_IMAGE_RES); + // Just like we do when binding views at the grid, we set the transition name to be the + // string + // value of the image res. + view.findViewById(R.id.image).setTransitionName(String.valueOf(imageRes)); + + // Load the image with Glide to prevent OOM error when the image drawables are very large. + Glide.with(this) + .load(imageRes) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, + Target target, boolean isFirstResource) { + // The postponeEnterTransition is called on the parent + // ImagePagerFragment, so the + // startPostponedEnterTransition() should also be called on it to get the + // transition + // going in case of a failure. + getParentFragment().startPostponedEnterTransition(); + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, + Target target, DataSource dataSource, + boolean isFirstResource) { + // The postponeEnterTransition is called on the parent + // ImagePagerFragment, so the + // startPostponedEnterTransition() should also be called on it to get the + // transition + // going when the image is ready. + getParentFragment().startPostponedEnterTransition(); + return false; + } + }).into((android.widget.ImageView) view.findViewById(R.id.image)); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } +} diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImagePagerFragment.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImagePagerFragment.java new file mode 100644 index 0000000..7753ea1 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/fragment/ImagePagerFragment.java @@ -0,0 +1,103 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition.fragment; + +import android.os.Bundle; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.app.SharedElementCallback; +import androidx.fragment.app.Fragment; +import androidx.viewpager.widget.ViewPager; +import com.wintmain.wBasis.R; +import com.wintmain.wBasis.transition.GridToPagerMainActivity; +import com.wintmain.wBasis.transition.adapter.ImagePagerAdapter; + +import java.util.List; +import java.util.Map; + +public class ImagePagerFragment extends Fragment { + private ViewPager viewPager; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + viewPager = (ViewPager) inflater.inflate(R.layout.fragment_grid_pager_view, container, false); + viewPager.setAdapter(new ImagePagerAdapter(this)); + // Set the current position and add a listener that will update the selection coordinator + // when + // paging the images. + viewPager.setCurrentItem(GridToPagerMainActivity.currentPosition); + + viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageSelected(int position) { + GridToPagerMainActivity.currentPosition = position; + } + }); + + prepareSharedElementTransition(); + + return viewPager; + } + + /** + * Prepares the shared element transition from and back to the grid fragment. + */ + private void prepareSharedElementTransition() { + Transition transition = + TransitionInflater.from(getContext()) + .inflateTransition(R.transition.image_shared_element_transition); + setSharedElementEnterTransition(transition); + + // A similar mapping is set at the GridFragment with a setExitSharedElementCallback. + setEnterSharedElementCallback( + new SharedElementCallback() { + @Override + public void onMapSharedElements(List names, + Map sharedElements) { + // Locate the image view at the primary fragment (the ImageFragment that + // is currently + // visible). To locate the fragment, call instantiateItem with the + // selection position. + // At this stage, the method will simply return the fragment at the + // position and will + // not create a new one. + Fragment currentFragment = (Fragment) viewPager.getAdapter() + .instantiateItem(viewPager, + GridToPagerMainActivity.currentPosition); + View view = currentFragment.getView(); + if (view == null) { + return; + } + + // Map the first shared element name to the child ImageView. + sharedElements.put(names.get(0), view.findViewById(R.id.image)); + } + }); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + } +} diff --git a/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/ImageData.java b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/ImageData.java new file mode 100644 index 0000000..baa10d8 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/java/com/wintmain/wBasis/transition/util/ImageData.java @@ -0,0 +1,53 @@ +/* + * Copyright 2023-2025 wintmain + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.wintmain.wBasis.transition.util; + +import androidx.annotation.DrawableRes; +import com.wintmain.wBasis.R; + +/** + * Holds the image resource references used by the grid and the pager fragments. + */ +public abstract class ImageData { + + // Image assets (free for commercial use, no attribution required, from pixabay.com) + @DrawableRes + public static final int[] IMAGE_DRAWABLES = { + R.drawable.icon_badminton, + R.drawable.icon_baseball, + R.drawable.icon_basketball, + R.drawable.icon_bowling, + R.drawable.icon_cycling, + R.drawable.icon_golf, + R.drawable.icon_running, + R.drawable.icon_soccer, + R.drawable.icon_swimming, + R.drawable.icon_tabletennis, + R.drawable.icon_tennis, + R.drawable.img_badminton, + R.drawable.img_baseball, + R.drawable.img_basketball, + R.drawable.img_bowling, + R.drawable.img_cycling, + R.drawable.img_golf, + R.drawable.img_running, + R.drawable.img_soccer, + R.drawable.img_swimming, + R.drawable.img_tabletennis, + R.drawable.img_tennis, + }; +} diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_grid_to_pager_main.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_grid_to_pager_main.xml new file mode 100644 index 0000000..bb622d1 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/activity_grid_to_pager_main.xml @@ -0,0 +1,20 @@ + + + diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_image_view.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_image_view.xml new file mode 100644 index 0000000..1a11c07 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_image_view.xml @@ -0,0 +1,22 @@ + + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_pager_view.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_pager_view.xml new file mode 100644 index 0000000..2dc44b2 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_pager_view.xml @@ -0,0 +1,20 @@ + + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_to_pager.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_to_pager.xml new file mode 100644 index 0000000..8b896fc --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/fragment_grid_to_pager.xml @@ -0,0 +1,27 @@ + + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/res-transition/layout/view_image_card.xml b/app-catalog/samples/wBasis/src/main/res-transition/layout/view_image_card.xml new file mode 100644 index 0000000..71edfa0 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/layout/view_image_card.xml @@ -0,0 +1,39 @@ + + + + + + diff --git a/app-catalog/samples/wBasis/src/main/res-transition/transition/grid_exit_transition.xml b/app-catalog/samples/wBasis/src/main/res-transition/transition/grid_exit_transition.xml new file mode 100644 index 0000000..5ffe1f4 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/transition/grid_exit_transition.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/res-transition/transition/image_shared_element_transition.xml b/app-catalog/samples/wBasis/src/main/res-transition/transition/image_shared_element_transition.xml new file mode 100644 index 0000000..e30d095 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/transition/image_shared_element_transition.xml @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/res-transition/values/colors.xml b/app-catalog/samples/wBasis/src/main/res-transition/values/colors.xml new file mode 100644 index 0000000..9e84818 --- /dev/null +++ b/app-catalog/samples/wBasis/src/main/res-transition/values/colors.xml @@ -0,0 +1,21 @@ + + + + #3F51B5 + #303F9F + #FF4081 + \ No newline at end of file diff --git a/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml b/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml index 093d36b..bcfaaec 100644 --- a/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml +++ b/app-catalog/samples/wBasis/src/main/res-transition/values/dimens.xml @@ -34,4 +34,5 @@ @dimen/margin_medium 4dp + 2 diff --git a/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml b/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml index 0a33a27..a77b517 100644 --- a/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml +++ b/app-catalog/samples/wBasis/src/main/res-transition/values/strings.xml @@ -84,4 +84,5 @@ shank pork loin jerky venison tenderloin boudin ham hock ground round. %1$s by %2$s + Image diff --git a/app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml b/app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml index 7d4aab5..e375bce 100644 --- a/app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml +++ b/app-catalog/samples/wBasis/src/main/res-transition/values/styles.xml @@ -29,4 +29,12 @@ + + + \ No newline at end of file