世界上没有悲伤的故事
比ViewPager'e和SET'e的故事
我想警告作者是一个新手android,所以本文包含了许多技术上的不准确之处,您应该警告您,技术上可靠的声明可能会出现在文章中。
后端在哪里
我一生都看到了后端。 2019年初,已经有一个非常雄心勃勃但尚未完成的项目。 一次前往苏黎世的无结果旅行,采访了一家搜索公司。 冬天,泥土,没有心情。 没有力量和渴望进一步推动该项目。
我想永远忘记这个可怕的后端。 幸运的是,命运给了我一个主意-那是一个移动应用程序。 它的主要功能是非标准使用相机。 工作开始沸腾了。 花了一些时间,现在原型已经准备就绪。 该项目即将发布,一切都很好,结构合理,直到我决定让用户“ 方便 ”为止。
没有人想要在2019年单击小菜单按钮,他们想左右滑动屏幕。 据说-做,做-坏了。 因此,第一个ViewPager
出现在我的项目中(我为与我相同的后端解密了一些术语-只需移动光标即可)。 共享元素过渡 (以下称为SET或过渡) ViewPager
Design的签名元素,断然拒绝与ViewPager
配合ViewPager
,这让我可以选择:在屏幕之间滑动或漂亮的过渡动画。 我不想拒绝任何一个。 于是我的搜索开始了。
学习时间:论坛上的数十个主题和StackOverflow上的未解答问题。 无论我打开什么,都可以从RecyclerView过渡到ViewPager或“附加芭蕉Fragment.postponeEnterTransition()
”。
民间补救措施无济于事,所以我决定自己调和ViewPager
和Shared Element Transition
。
我开始反思:“问题出现在用户从一页转到另一页的那一刻……”。 然后我想到:“如果不更改页面,您在设置页面时不会遇到SET问题。”
我们可以在同一页面上进行转换 ,然后在ViewPager
当前页面替换为目标页面。
首先,创建我们将使用的片段 。
SmallPictureFragment small_picture_fragment = new SmallPictureFragment(); BigPictureFragment big_picture_fragment = new BigPictureFragment();
让我们尝试将当前页面中的片段更改为其他内容。
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
我们启动该应用程序,然后...平稳切换到黑屏。 是什么原因
事实证明,每个页面的容器都是ViewPager
本身,没有诸如Page1Container
, Page2Container
类的任何中介。 因此,仅将一页更改为另一页不起作用,将替换整个pager
。
好吧,要单独更改每个页面的内容,我们为每个页面创建几个容器片段。
RootSmallPictureFragment root_small_pic_fragment = new RootSmallPictureFragment(); RootBigPictureFragment root_big_pic_fragment = new RootBigPictureFragment();
某些事情将不会再次开始。
java.lang.IllegalStateException:无法更改片段BigPictureFragment {...}的容器ID:现在是2131165289,现在是2131165290
我们无法将第二页的片段( BigPictureFragment
)附加到第一页,因为它已经附加到第二页的容器中。
咬紧牙齿后,我们添加了更多的双碎片。
SmallPictureFragment small_picture_fragment_fake = new SmallPictureFragment(); BigPictureFragment big_picture_fragment_fake = new BigPictureFragment();
赚了! 我曾经从GitHub复制的过渡代码已经包含淡入和淡出动画。 因此,在过渡之前,第一个片段中的所有静态元素都消失了,然后图片移动了,然后才出现第二页的元素。 对用户而言,这看起来像页面之间的真实移动。
所有动画均已通过,但是存在一个问题。 用户仍在第一页上,但应该在第二页上。
为了解决这个问题,我们小心地将可见的ViewPager
页面ViewPager
为第二个页面。 然后,我们将第一页的内容还原到其初始状态。
handler.postDelayed( () -> {
整个源代码 | public class FragmentTransitionUtil { |
| |
| private static final long FADE_DEFAULT_TIME = 500; |
| private static final long MOVE_DEFAULT_TIME = 1000; |
| |
| public static void perform( |
| MainActivity activity, |
| Fragment previousFragment, |
| Fragment nextFragment, |
| Map<View, String> sharedElements, |
| int nextPage |
| |
| ) { |
| FragmentManager fragmentManager = activity.getSupportFragmentManager(); |
| FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); |
| |
| if (previousFragment != null) { |
| |
| // 1. Exit for Previous Fragment |
| Fade exitFade = new Fade(); |
| exitFade.setDuration(FADE_DEFAULT_TIME); |
| previousFragment.setExitTransition(exitFade); |
| |
| // 2. Shared Elements Transition |
| TransitionSet enterTransitionSet = new TransitionSet(); |
| enterTransitionSet.addTransition( |
| new TransitionSet() { |
| |
| { |
| setOrdering(ORDERING_TOGETHER); |
| addTransition(new ChangeBounds()). |
| addTransition(new ChangeTransform()). |
| addTransition(new ChangeImageTransform()); |
| } |
| } |
| ); |
| |
| enterTransitionSet.setDuration(MOVE_DEFAULT_TIME); |
| enterTransitionSet.setStartDelay(FADE_DEFAULT_TIME); |
| nextFragment.setSharedElementEnterTransition(enterTransitionSet); |
| |
| // 3. Enter Transition for New Fragment |
| Fade enterFade = new Fade(); |
| enterFade.setStartDelay(MOVE_DEFAULT_TIME + FADE_DEFAULT_TIME); |
| enterFade.setDuration(FADE_DEFAULT_TIME); |
| nextFragment.setEnterTransition(enterFade); |
| |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { |
| if (sharedElements != null) { |
| for (Map.Entry<View, String> viewStringEntry : sharedElements.entrySet()) { |
| View view = viewStringEntry.getKey(); |
| String transName = viewStringEntry.getValue(); |
| view.setTransitionName(transName); |
| |
| fragmentTransaction.addSharedElement( |
| view, |
| transName |
| ); |
| } |
| } |
| } |
| |
| int fragmentContId = ((ViewGroup) previousFragment.getView().getParent()).getId(); |
| fragmentTransaction.replace(fragmentContId, nextFragment); |
| fragmentTransaction.commit(); |
| |
| |
| final Handler handler = new Handler(); |
| handler.postDelayed( |
| () -> { |
| // Stealthy changing page visible to user. He won’t notice! |
| activity.viewPager.setCurrentItem(nextPage, false); |
| FragmentTransaction transaction = fragmentManager.beginTransaction(); |
| // Restore previous fragment. It contains inappropriate view now |
| transaction.replace(fragmentContId, previousFragment); |
| transaction.commitAllowingStateLoss(); |
| }, |
| FADE_DEFAULT_TIME + MOVE_DEFAULT_TIME + FADE_DEFAULT_TIME |
| ); |
| |
| |
| } |
| } |
| |
| } |
可以在GitHub上查看该项目。
总结一下。 该代码开始看起来更加可靠:我得到的不是原来的2个片段,而是6个,其中出现了控制性能的指令,并在适当的时候替换了片段。 而这仅在演示中。
在同一项目中,在代码中一个接一个地备份,备份开始出现在最意外的位置。 当用户单击“错误”页面上的按钮或限制重复片段的后台工作时,它们不允许应用程序崩溃。
事实证明,android没有回调来完成过渡 ,并且它的执行时间是非常任意的,并且取决于许多因素(例如, RecyclerView
在生成的片段中加载的速度如何)。 这导致了这样一个事实,在handler.postDelayed()
对片段的替换通常执行得太早或太晚,这只会加剧先前的问题。
最后一个亮点是,在动画过程中,用户可以简单地滑动到另一页并观看两个双屏,然后应用程序还将其拉到所需的屏幕上。
这种状况不适合我,我充满义愤填began,开始寻求另一种解决方案。
尝试PageTransformer
互联网上仍然没有答案,我想:如何才能进一步推动这一转变。 意识皮层中的某事对我低声说:“使用PageTransformer
,Luke。” 这个想法对我来说似乎很有希望,我决定听一听。
想法是制作PageTransformer
,它与Android SET不同,在过渡的两边都不需要多次重复setTransitionName(transitionName)
和FragmentTransaction.addSharedElement(sharedElement,name)
。 滑动后它将移动元素,并具有以下形式的简单界面:
public void addSharedTransition(int fromViewId, int toViewId)
我们着手发展。 我将把addSharedTransition(fromId, toId)
方法中的数据保存为“从Pair
Set
,并在addSharedTransition(fromId, toId)
方法中获取它
public void transformPage(@NonNull View page, float position)
在内部,我将浏览所有已保存的View
对,在这些对之间我需要制作动画。 而且,我将尝试过滤它们,以便仅对可见元素进行动画处理。
首先,让我们检查是否已创建需要动画的元素。 我们并不挑剔,并且如果不是在动画开始之前创建View
,则不会破坏整个动画(如Shared Element Transition ),而是在创建元素时将其拾取。
for (Pair<Integer,Integer> idPair : sharedElementIds) { Integer fromViewId = idPair.first; Integer toViewId = idPair.second; View fromView = activity.findViewById(fromViewId); View toView = activity.findViewById(toViewId); if (fromView != null && toView != null) {
我找到发生移动的页面(下面将介绍如何确定页码)。
View fromPage = pages.get(fromPageNumber); View toPage = pages.get(toPageNumber);
如果两个页面都已经创建,那么我正在寻找一对需要动画的View
。
If (fromPage != null && toPage != null) { fromView = fromPage.findViewById(fromViewId); toView = toPage.findViewById(toViewId);
在此阶段,我们选择了“视图”,它位于用户在其间滚动的页面上。
现在该获取很多变量了。 我计算参考点:
在最后一个代码片段中,我设置了slideToTheRight
,并且在其中已经对我很有用。 登录translation
取决于它,它确定View
将飞到其位置还是屏幕之外。
float pageWidth = getScreenWidth(); float sign = slideToTheRight ? 1 : -1; float translationY = (deltaY + deltaHeight / 2) * sign * (-position); float translationX = (deltaX + sign * pageWidth + deltaWidth / 2) * sign * (-position);
有趣的是,尽管初始偏移不同,但起始页面和结果页面上View
X
和Y
的偏移公式却是相同的。
但是不幸的是,使用缩放比例时,此技巧将不起作用-您需要考虑此View
动画View
起点还是终点。
这可能会让某人感到意外,但是transformPage(@NonNull View page, float position)
被调用了很多次:对于每个缓存的页面(缓存大小是可自定义的)。 并且,为了不多次重绘动画View
,对于每次对transformPage()
调用,我们仅更改当前page
上的那些。
选择要绘制动画的页面
ViewPager
不急于在正在滚动的页面之间共享信息。 如我所言,现在我将告诉您我们如何获得此信息。 在我们的ViewPager.OnPageChangeListener
PageTransformer
我们实现了另一个ViewPager.OnPageChangeListener
接口。 通过System.out.println()
研究了onPageScrolled()
的输出后System.out.println()
我得出以下公式:
public void onPageScrolled( int position, float positionOffset, int positionOffsetPixels ) { Set<Integer> visiblePages = new HashSet<>(); visiblePages.add(position); visiblePages.add(positionOffset >= 0 ? position + 1 : position - 1); visiblePages.remove(fromPageNumber); toPageNumber = visiblePages.iterator().next(); if (pages == null || toPageNumber >= pages.size()) toPageNumber = null; } public void onPageSelected(int position) { this.position = position; } public void onPageScrollStateChanged(int state) { if (state == SCROLL_STATE_IDLE) {
仅此而已。 我们做到了! 动画监视用户手势。 当您可以保留所有内容时,为什么要在滑动和共享元素过渡之间进行选择。
在撰写本文时,我添加了静态元素消失的效果-它仍然非常粗糙,因此尚未添加到库中。
整个源代码 | /** |
| * PageTransformer that allows you to do shared element transitions between pages in ViewPager. |
| * It requires view pager sides match screen sides to function properly. I.e. ViewPager page width |
| * must be equal to screen width. <br/> |
| * Usage:<br/> |
| * <code> |
| * sharedElementPageTransformer.addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code> |
| * </code> |
| * |
| * |
| */ |
| public class SharedElementPageTransformer implements ViewPager.PageTransformer, ViewPager.OnPageChangeListener { |
| /** Android need the correction while view scaling for some reason*/ |
| private static float MAGICAL_ANDROID_RENDERING_SCALE = 1; |
| // private static float MAGICAL_ANDROID_RENDERING_SCALE = 0.995f; |
| |
| // External variables |
| |
| private final Activity activity; |
| List<Fragment> fragments; |
| private Set<Pair<Integer,Integer>> sharedElementIds = new HashSet<>(); |
| |
| |
| |
| //Internal variables |
| |
| private List<View> pages; |
| private Map<View, Integer> pageToNumber = new HashMap<>(); |
| |
| private Integer fromPageNumber = 0; |
| private Integer toPageNumber; |
| |
| /** current view pager position */ |
| private int position; |
| |
| /** |
| * @param activity activity that hosts view pager |
| * @param fragments fragment that are in view pager in the SAME ORDER |
| */ |
| public SharedElementPageTransformer(Activity activity, List<Fragment> fragments) { |
| this.activity = activity; |
| this.fragments = fragments; |
| } |
| |
| |
| @Override |
| public void transformPage(@NonNull View page, float position) { |
| updatePageCache(); |
| if (fromPageNumber == null || toPageNumber == null) return; |
| |
| for (Pair<Integer,Integer> idPair : sharedElementIds) { |
| Integer fromViewId = idPair.first; |
| Integer toViewId = idPair.second; |
| |
| View fromView = activity.findViewById(fromViewId); |
| View toView = activity.findViewById(toViewId); |
| |
| if (fromView != null && toView != null) { |
| //Looking if current Shared element transition matches visible pages |
| |
| View fromPage = pages.get(fromPageNumber); |
| View toPage = pages.get(toPageNumber); |
| |
| if (fromPage != null && toPage != null) { |
| fromView = fromPage.findViewById(fromViewId); |
| toView = toPage.findViewById(toViewId); |
| |
| |
| // if both views are on pages user drag between apply transformation |
| if ( |
| fromView != null |
| && toView != null |
| ) { |
| // saving shared element position on the screen |
| float fromX = fromView.getX() - fromView.getTranslationX(); |
| float fromY = fromView.getY() - fromView.getTranslationY(); |
| float toX = toView.getX() - toView.getTranslationX(); |
| float toY = toView.getY() - toView.getTranslationY(); |
| float deltaX = toX - fromX; |
| float deltaY = toY - fromY; |
| |
| // scaling |
| float fromWidth = fromView.getWidth(); |
| float fromHeight = fromView.getHeight(); |
| float toWidth = toView.getWidth(); |
| float toHeight = toView.getHeight(); |
| float deltaWidth = toWidth - fromWidth; |
| float deltaHeight = toHeight - fromHeight; |
| |
| |
| int fromId = fromView.getId(); |
| int toId = toView.getId(); |
| |
| boolean slideToTheRight = toPageNumber > fromPageNumber; |
| |
| if (position <= -1) { |
| |
| } else if (position < 1) { |
| |
| float pageWidth = getSceenWidth(); |
| float sign = slideToTheRight ? 1 : -1; |
| |
| float translationY = (deltaY + deltaHeight / 2) * sign * (-position); |
| float translationX = (deltaX + sign * pageWidth + deltaWidth / 2) * sign * (-position); |
| |
| if (page.findViewById(fromId) != null) { |
| fromView.setTranslationX(translationX); |
| fromView.setTranslationY(translationY); |
| |
| float scaleX = (fromWidth == 0) ? 1 : (fromWidth + deltaWidth * sign * (-position)) / fromWidth; |
| float scaleY = (fromHeight == 0) ? 1 : (fromHeight + deltaHeight * sign * (-position)) / fromHeight; |
| |
| fromView.setScaleX(scaleX); |
| fromView.setScaleY(scaleY * MAGICAL_ANDROID_RENDERING_SCALE); |
| } |
| if (page.findViewById(toId) != null) { |
| |
| toView.setTranslationX(translationX); |
| toView.setTranslationY(translationY); |
| float scaleX = (toWidth == 0) ? 1 : (toWidth + deltaWidth * sign * (-position)) / toWidth; |
| float scaleY = (toHeight == 0) ? 1 :(toHeight + deltaHeight * sign * (-position)) / toHeight; |
| |
| toView.setScaleX(scaleX); |
| toView.setScaleY(scaleY); |
| } |
| |
| |
| } else { |
| } |
| |
| } |
| } |
| } |
| } |
| } |
| |
| private float getSceenWidth() { |
| Point outSize = new Point(); |
| activity.getWindowManager().getDefaultDisplay().getSize(outSize); |
| return outSize.x; |
| } |
| |
| /** |
| * Creating page cache array to determine if shared element on |
| * currently visible page |
| */ |
| private void updatePageCache() { |
| pages = new ArrayList<>(); |
| |
| for (int i = 0; i < fragments.size(); i++) { |
| View pageView = fragments.get(i).getView(); |
| pages.add(pageView); |
| pageToNumber.put(pageView, i); |
| |
| } |
| } |
| |
| |
| @Override |
| public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { |
| Set<Integer> visiblePages = new HashSet<>(); |
| |
| visiblePages.add(position); |
| visiblePages.add(positionOffset >= 0 ? position + 1 : position - 1); |
| visiblePages.remove(fromPageNumber); |
| |
| toPageNumber = visiblePages.iterator().next(); |
| |
| if (pages == null || toPageNumber >= pages.size()) toPageNumber = null; |
| } |
| |
| |
| @Override |
| public void onPageSelected(int position) { |
| this.position = position; |
| } |
| |
| @Override |
| public void onPageScrollStateChanged(int state) { |
| if (state == SCROLL_STATE_IDLE) { |
| fromPageNumber = position; |
| resetViewPositions(); |
| } |
| |
| } |
| |
| private void resetViewPositions() { |
| for (Pair<Integer, Integer> idPair : sharedElementIds) { |
| View sharedElement = activity.findViewById(idPair.first); |
| if(sharedElement != null) { |
| sharedElement.setTranslationX(0); |
| sharedElement.setTranslationY(0); |
| sharedElement.setScaleX(1); |
| sharedElement.setScaleY(1); |
| } |
| sharedElement = activity.findViewById(idPair.second); |
| if(sharedElement != null) { |
| |
| sharedElement.setTranslationX(0); |
| sharedElement.setTranslationY(0); |
| sharedElement.setScaleX(1); |
| sharedElement.setScaleY(1); |
| } |
| } |
| |
| } |
| |
| /** |
| * Set up shared element transition from element with <code>fromViewId</code> to |
| * element with <code>toViewId</code>. Note that you can setup each transition |
| * direction separately. e.g. <br/> |
| * <code>addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code><br/> |
| * and<br/> |
| * <code>addSharedTransition(R.id.SecondPageTextView, R.id.FirstPageTextView)</code><br/> |
| * are different. |
| * @param fromViewId |
| * @param toViewId |
| */ |
| public void addSharedTransition(int fromViewId, int toViewId) { |
| addSharedTransition(fromViewId, toViewId, false); |
| } |
| |
| /** |
| * Set up shared element transition from element with <code>fromViewId</code> to |
| * element with <code>toViewId</code>. Note that you can setup each transition |
| * direction separately. e.g. <br/> |
| * <code>addSharedTransition(R.id.FirstPageTextView, R.id.SecondPageTextView)</code><br/> |
| * and<br/> |
| * <code>addSharedTransition(R.id.SecondPageTextView, R.id.FirstPageTextView)</code><br/> |
| * are different. |
| * @param fromViewId |
| * @param toViewId |
| * @param bothDirections to include backward transition from toViewId to fromViewId aswell |
| */ |
| public void addSharedTransition(int fromViewId, int toViewId, boolean bothDirections) { |
| sharedElementIds.add(new Pair<>(fromViewId, toViewId)); |
| if(bothDirections) { |
| sharedElementIds.add(new Pair<>(toViewId, fromViewId)); |
| } |
| } |
| /** |
| * In case there is "ladder" appears between while transition. |
| * You may try to tune that magical scale to get rid of it. |
| * @param magicalAndroidRenderingScale float between 0 and infinity. Typically very close to 1.0 |
| */ |
| public static void setMagicalAndroidRenderingScale(float magicalAndroidRenderingScale) { |
| MAGICAL_ANDROID_RENDERING_SCALE = magicalAndroidRenderingScale; |
| } |
| } |
库的工作原理如下
该配置非常简洁。
我们的示例的完整设置如下所示 ArrayList<Fragment> fragments = new ArrayList<>(); fragments.add(hello_fragment); fragments.add(small_picture_fragment); fragments.add(big_picture_fragment); SharedElementPageTransformer transformer = new SharedElementPageTransformer(this, fragments); transformer.addSharedTransition(R.id.smallPic_image_cat2, R.id.bigPic_image_cat, true); transformer.addSharedTransition(R.id.smallPic_text_label3, R.id.bigPic_text_label, true); transformer.addSharedTransition(R.id.hello_text, R.id.smallPic_text_label3, true); viewPager.setPageTransformer(false, transformer); viewPager.addOnPageChangeListener(transformer);
onClick
(包括所有转换)可能看起来像这样:
smallCatImageView.setOnClickListener( v -> activity.viewPager.setCurrentItem(2) );
为了使代码不会消失,我将库放在JCenter存储库和GitHub上 。 因此,我与开源世界取得了联系。 您只需添加即可在项目中进行尝试
dependencies {
所有资源都可以在GitHub上找到
结论
即使Internet不知道答案,也不意味着不是。 寻找解决方法,尝试直到解决。 也许您将是第一个深入了解并与社区分享的人。