1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.providers.media.photopicker.ui; 17 18 import static com.android.providers.media.util.MimeUtils.isVideoMimeType; 19 20 import android.graphics.Color; 21 import android.graphics.drawable.GradientDrawable; 22 import android.graphics.drawable.LayerDrawable; 23 import android.os.Bundle; 24 import android.provider.MediaStore; 25 import android.util.Log; 26 import android.view.LayoutInflater; 27 import android.view.View; 28 import android.view.ViewGroup; 29 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import androidx.core.content.ContextCompat; 33 import androidx.fragment.app.Fragment; 34 import androidx.fragment.app.FragmentManager; 35 import androidx.fragment.app.FragmentTransaction; 36 import androidx.lifecycle.ViewModelProvider; 37 import androidx.recyclerview.widget.RecyclerView; 38 import androidx.viewpager2.widget.CompositePageTransformer; 39 import androidx.viewpager2.widget.ViewPager2; 40 41 import com.android.providers.media.R; 42 import com.android.providers.media.photopicker.util.AccentColorResources; 43 import com.android.providers.media.photopicker.util.MimeFilterUtils; 44 import com.android.providers.media.photopicker.viewmodel.PickerViewModel; 45 46 import com.google.android.material.bottomsheet.BottomSheetBehavior; 47 import com.google.android.material.tabs.TabLayout; 48 import com.google.android.material.tabs.TabLayoutMediator; 49 50 import java.lang.ref.WeakReference; 51 import java.lang.reflect.Field; 52 53 /** 54 * The tab container fragment 55 */ 56 public class TabContainerFragment extends Fragment { 57 private static final String TAG = "TabContainerFragment"; 58 private static final int PHOTOS_TAB_POSITION = 0; 59 private static final int ALBUMS_TAB_POSITION = 1; 60 61 private TabContainerAdapter mTabContainerAdapter; 62 private TabLayoutMediator mTabLayoutMediator; 63 private ViewPager2 mViewPager; 64 private PickerViewModel mPickerViewModel; 65 66 @Override 67 @NonNull onCreateView(@onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)68 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, 69 Bundle savedInstanceState) { 70 super.onCreateView(inflater, container, savedInstanceState); 71 return inflater.inflate(R.layout.fragment_picker_tab_container, container, false); 72 } 73 74 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)75 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 76 super.onViewCreated(view, savedInstanceState); 77 mViewPager = view.findViewById(R.id.picker_tab_viewpager); 78 final ViewModelProvider viewModelProvider = new ViewModelProvider(requireActivity()); 79 mPickerViewModel = viewModelProvider.get(PickerViewModel.class); 80 mTabContainerAdapter = new TabContainerAdapter(/* fragment */ this); 81 mViewPager.setAdapter(mTabContainerAdapter); 82 83 // Launch in albums tab if the app requests so 84 if (mPickerViewModel.getPickerLaunchTab() == MediaStore.PICK_IMAGES_TAB_ALBUMS) { 85 // Launch the picker in Albums tab without any switch animation 86 mViewPager.setCurrentItem(ALBUMS_TAB_POSITION, /* smoothScroll */ false); 87 } 88 89 // If the ViewPager2 has more than one page with BottomSheetBehavior, the scrolled view 90 // (e.g. RecyclerView) on the second page can't be scrolled. The workaround is to update 91 // nestedScrollingChildRef to the scrolled view on the current page. b/145334244 92 Field fieldNestedScrollingChildRef = null; 93 try { 94 fieldNestedScrollingChildRef = BottomSheetBehavior.class.getDeclaredField( 95 "nestedScrollingChildRef"); 96 fieldNestedScrollingChildRef.setAccessible(true); 97 } catch (NoSuchFieldException ex) { 98 Log.d(TAG, "Can't get the field nestedScrollingChildRef from BottomSheetBehavior", ex); 99 } 100 101 final BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from( 102 getActivity().findViewById(R.id.bottom_sheet)); 103 104 final CompositePageTransformer compositePageTransformer = new CompositePageTransformer(); 105 mViewPager.setPageTransformer(compositePageTransformer); 106 compositePageTransformer.addTransformer(new AnimationPageTransformer()); 107 compositePageTransformer.addTransformer( 108 new NestedScrollPageTransformer(bottomSheetBehavior, fieldNestedScrollingChildRef)); 109 110 // The BottomSheetBehavior looks for the first nested scrolling child to determine how to 111 // handle nested scrolls, it finds the inner recyclerView on ViewPager2 in this case. So, we 112 // need to work around it by setNestedScrollingEnabled false. b/145351873 113 final View firstChild = mViewPager.getChildAt(0); 114 if (firstChild instanceof RecyclerView) { 115 mViewPager.getChildAt(0).setNestedScrollingEnabled(false); 116 } 117 118 final TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout); 119 120 if (mPickerViewModel.getPickerAccentColorParameters().isCustomPickerColorSet()) { 121 tabLayout.setBackgroundColor( 122 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 123 AccentColorResources.SURFACE_CONTAINER_COLOR_LIGHT, 124 AccentColorResources.SURFACE_CONTAINER_COLOR_DARK)); 125 LayerDrawable pickerTabDrawable = (LayerDrawable) ContextCompat.getDrawable( 126 getActivity(), R.drawable.picker_tab_background); 127 GradientDrawable pickerTab = 128 (GradientDrawable) pickerTabDrawable.findDrawableByLayerId(R.id.picker_tab); 129 pickerTab.setColor(mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 130 AccentColorResources.SURFACE_CONTAINER_HIGHEST_LIGHT, 131 AccentColorResources.SURFACE_CONTAINER_HIGHEST_DARK)); 132 tabLayout.setSelectedTabIndicatorColor( 133 mPickerViewModel.getPickerAccentColorParameters().getPickerAccentColor()); 134 setTabTextColors(tabLayout); 135 } 136 137 mTabLayoutMediator = new TabLayoutMediator(tabLayout, mViewPager, (tab, pos) -> { 138 if (pos == PHOTOS_TAB_POSITION) { 139 if (isOnlyVideoMimeTypeFilterAvailable()) { 140 tab.setText(R.string.picker_videos); 141 } else { 142 tab.setText(R.string.picker_photos); 143 } 144 } else if (pos == ALBUMS_TAB_POSITION) { 145 tab.setText(R.string.picker_albums); 146 } 147 }); 148 149 mTabLayoutMediator.attach(); 150 // TabLayout only supports colorDrawable in xml. And if we set the color in the drawable by 151 // setSelectedTabIndicator method, it doesn't apply the color. So, we set color in xml and 152 // set the drawable for the shape here. 153 tabLayout.setSelectedTabIndicator(R.drawable.picker_tab_indicator); 154 tabLayout.addOnTabSelectedListener(mOnTabSelectedListener); 155 } 156 setTabTextColors(TabLayout tabLayout)157 private void setTabTextColors(TabLayout tabLayout) { 158 String selectedTabTextColor = 159 mPickerViewModel.getPickerAccentColorParameters().isAccentColorBright() 160 ? AccentColorResources.DARK_TEXT_COLOR 161 : AccentColorResources.LIGHT_TEXT_COLOR; 162 // Unselected tab text color in dark mode will be white and vice versa 163 tabLayout.setTabTextColors( 164 mPickerViewModel.getPickerAccentColorParameters().getThemeBasedColor( 165 AccentColorResources.DARK_TEXT_COLOR, 166 AccentColorResources.LIGHT_TEXT_COLOR), 167 Color.parseColor(selectedTabTextColor)); 168 } 169 isOnlyVideoMimeTypeFilterAvailable()170 private boolean isOnlyVideoMimeTypeFilterAvailable() { 171 String [] mimeTypeFilters = MimeFilterUtils.getMimeTypeFilters(getActivity().getIntent()); 172 boolean hasVideoMimeTypeFilterOnly = false; 173 if (mimeTypeFilters != null && mimeTypeFilters.length > 0) { 174 for (String mimeTypeFilter : mimeTypeFilters) { 175 if (isVideoMimeType(mimeTypeFilter)) { 176 hasVideoMimeTypeFilterOnly = true; 177 } else { 178 hasVideoMimeTypeFilterOnly = false; 179 break; 180 } 181 } 182 } 183 return hasVideoMimeTypeFilterOnly; 184 } 185 186 @Override onDestroyView()187 public void onDestroyView() { 188 mTabLayoutMediator.detach(); 189 super.onDestroyView(); 190 } 191 192 /** 193 * Create the fragment and add it into the FragmentManager 194 * 195 * @param fm the fragment manager 196 */ show(FragmentManager fm)197 public static void show(FragmentManager fm) { 198 final FragmentTransaction ft = fm.beginTransaction(); 199 final TabContainerFragment fragment = new TabContainerFragment(); 200 ft.replace(R.id.fragment_container, fragment, TAG); 201 ft.commitAllowingStateLoss(); 202 } 203 204 private final TabLayout.OnTabSelectedListener mOnTabSelectedListener = 205 new TabLayout.OnTabSelectedListener() { 206 @Override 207 public void onTabSelected(TabLayout.Tab tab) { 208 int position = tab.getPosition(); 209 if (position == PHOTOS_TAB_POSITION) { 210 mPickerViewModel.logSwitchToPhotosTab(); 211 } else if (position == ALBUMS_TAB_POSITION) { 212 mPickerViewModel.logSwitchToAlbumsTab(); 213 } 214 } 215 216 @Override 217 public void onTabUnselected(TabLayout.Tab tab) { 218 // No=op 219 } 220 221 @Override 222 public void onTabReselected(TabLayout.Tab tab) { 223 // No-op 224 } 225 }; 226 227 private static class AnimationPageTransformer implements ViewPager2.PageTransformer { 228 229 @Override transformPage(@onNull View view, float pos)230 public void transformPage(@NonNull View view, float pos) { 231 view.setAlpha(1.0f - Math.abs(pos)); 232 } 233 } 234 235 private static class NestedScrollPageTransformer implements ViewPager2.PageTransformer { 236 private Field mFieldNestedScrollingChildRef; 237 private BottomSheetBehavior mBottomSheetBehavior; 238 NestedScrollPageTransformer(BottomSheetBehavior bottomSheetBehavior, Field field)239 public NestedScrollPageTransformer(BottomSheetBehavior bottomSheetBehavior, Field field) { 240 mBottomSheetBehavior = bottomSheetBehavior; 241 mFieldNestedScrollingChildRef = field; 242 } 243 244 @Override transformPage(@onNull View view, float pos)245 public void transformPage(@NonNull View view, float pos) { 246 // If pos != 0, it is not in current page, don't update the nested scrolling child 247 // reference. 248 if (pos != 0 || mFieldNestedScrollingChildRef == null) { 249 return; 250 } 251 252 try { 253 final View childView = view.findViewById(R.id.picker_tab_recyclerview); 254 if (childView != null) { 255 mFieldNestedScrollingChildRef.set(mBottomSheetBehavior, 256 new WeakReference(childView)); 257 } 258 } catch (IllegalAccessException ex) { 259 Log.d(TAG, "Set nestedScrollingChildRef to BottomSheetBehavior fail", ex); 260 } 261 } 262 } 263 } 264