admin管理员组

文章数量:1125742

I'm working on building a highly immersive media experience in Flutter using a combination of GridView, PageView, and VideoPlayer. My goal is to provide seamless transitions between these views, complete with animations for overscroll-to-pop, interactive image zooming, and intuitive tap-to-hide controls for both navigation bars and video playback.

For simplicity, I'm using Provider. However, I'm encountering jank during certain animations, particularly when transitioning from a displaying thumbnail to playing a video in the same page of the PageView as well as jank in the OverscrollPop widget.

I've outlined the key parts of my code and project structure below. My github repo has my full project you can take a look and/or contribute to some solutions :)

Homepage

MediaProvider is wrapped over the entire app in main.dart

body: Consumer<MediaProvider>(
 builder: (context, provider, child) {
  final List<Media> media = provider.media;

  return Scrollbar(
   child: RefreshIndicator(
    onRefresh: () async {},
    child: GridViewBuilder(media: media),
   ),
  );
 }
)

GridViewBuilder

This is a GridView.builder widget intended to handle the navigation from GridView to PageView

... return GestureDetector(
onTap: () => Navigator.push(
 context, PageRouteBuilder(
 opaque: true,
 pageBuilder: (context, animation, secondaryAnim) {
  mediaList: media,
  initialPage: index,
 }

transitionBuilder: (context, animation, secondaryAnim, child) {
 return FadeTransition(
  opacity: animation,
  child: child,
 );
})),
child: Hero(
 tag: media[index].docId
 child: Container(
  decoration: BoxDecoration(
   image: DecorationImage(
    image: NetworkImage(media[index].thumbUrl)
    fit: BoxFit.cover,  
   ),
  ),
  child: isVideo
   ? const Icon(Icons.play_arrow_rounded,
   : const SizedBox.shrink(),
...

MediaPageviewProvider Provider Class

MediaPageviewProvider is a temporary Provider class for the purpose of handling page controller, animation controller, transformation controller, DragToPopDirection, and TickerProvider

class MediaPageviewProvider extends ChangeNotifier {

MediaPageviewProvider({
 required this.mediaList,
 required this.dragToPopDirection,
 required int initialPage,
 required this.vysnc,
}) : pageController = PageController(initialPage: initialPage),
 transformationController = TransformationController(),
 animationController = AnimationController(
  vsync: vsync,
  duration: const Duration(milliseconds: 200),
 ),
 currentIndex = initialPage {
  transformationController.addListener(_onZoomChanged);
 }

void toggleControls() {}
void pageChanged(int page) {}
void doubletapToZoom() {}

// isZoomed is used to ensure that the PageView and OverscrollPop are disabled if the 
// image has been zoomed into
void _onZoomChanged() {
 final scale = transformationController.value.getMaxScaleOnAxis();
 isZoomed = scale > 1.0;
 notifyListeners();
}

// For the optional video, initialize and set the videoPlayerController and 
// notifylisteners
void initAndSetVideoPlayerController(String url) async {
 // ValueListener
 loadingVideo.value = true;
 notifyListeners();
 
 final controller = VideoPlayerControllerworkUrl(Uri.parse(url));
 await controller.initialize();
 videoController = controller;
 loadingVideo.value = false;
 notifyListeners();
}

void playPauseVideo() {}
...

PageviewPage

The bulk of the project exists here and i'll condense the code to make it easier to understand what's happening. Requires List<Media> mediaList and int intialPage.

// PageviewPageState Stateful Widget that extends SingleTickerProviderStsateMixin

Widget build(BuildContext context) {

return ChangeNotifierProvider(
 create: (_) => MediaPageviewProvider(
  mediaList: widget.mediaList,
  dragToPopDirection: DragToPopDirection.vertical,
  initialPage: widget.initialPage,
  vsync: this,
 ),
 child: Consumer<MediaPageviewProvider>(
  builder: (context, provider, child) {
   return Scaffold(
    // extendBody and extendBodyBehindAppBar are attempts to help the InteractiveViewer
    // playground
    extendBody: true,
    extendBodyBehindAppBar: true,
    body: OverscrollPop(
     enable: !provider.isZoomed,
     dragToPopDirection: provider.dragToPopDirection,
     child: GestureDetector(
      onTap: provider.toggleControls,
      onDoubleTap: provider.mediaList.length,
      child: PageView.builder(
       // assigned controller, itemCount, and onPageChanged,
       physics: provider.isZoomed 
        ? const NeverScrollableScrollPhysics()
        : const ClampingScrollPhysics(),
       itemBuilder: (context, index) {
        return ContentItem(
         media: provider.mediaList[index],
         provider: provider,
        );
       },
      ),
     ),
    ),
   );
  }
 ),
);}

ContentItem

This widget will change state depending on MediaType {image, video}, and provider.videoController != null. Requires Media media and MediaPageviewProvider provider.

if (provider.videoController != null) {
 return Stack(
  children: [
   AspectRatio(
    aspectRatio: controller.value.aspectRatio,
    child: VideoPlayer(controller),
   ),
   Positioned(
    bottom: 60 + kBottomNavigationBarHeight,
    left: 10,
    right: 10,
    child: VideoControls(
     // pass controller, provider.animationcontroller, and provider.showControls
    ),
   ),
  ],
 );
}

VideoControls

A simple stack widget with two sliders (buffer and interactive), a play/pause button, volume button, and duration/video length timestamp. This widget utilizes the animationController and showControls boolean from the MediaPageviewProvider to hide or show video controls.

本文标签: