2525 * a `building-3d` extrusion layer that renders 3D buildings at high zoom.
2626 */
2727
28- import { useCallback , useMemo , useRef , useState } from "react" ;
28+ import { memo , useCallback , useMemo , useRef , useState } from "react" ;
2929import {
3030 View ,
3131 Text ,
@@ -135,16 +135,13 @@ const MARKER_HITBOX = { width: 50, height: 50 } as const;
135135// 62% mid — the Google-Maps-style "selected place" height where the hero
136136// image, facts, and action row are visible. Index 2 = 100% — the sheet
137137// fills the screen all the way up behind the notch, sliding under the
138- // floating search pill. At that snap the list content gains a top
139- // padding equal to the pill height + safe area so rows never render
140- // underneath the pill. Magnetic snapping pulls a flick past mid straight
141- // to the top.
138+ // floating search pill. Magnetic snapping pulls a flick past mid
139+ // straight to the top.
142140const SHEET_SNAP_POINTS : Array < string | number > = [ "15%" , "62%" , "100%" ] ;
143- // Detail mode and list mode both use the mid snap so the camera padding
144- // stays consistent — the only difference is the rendered children.
145- const SHEET_INDEX_DETAIL = 1 ;
146- const SHEET_INDEX_LIST = 1 ;
147- const SHEET_INDEX_SEARCH = 2 ;
141+ const SHEET_INDEX_MID = 1 ;
142+ const SHEET_INDEX_TOP = 2 ;
143+
144+ const LIST_CONTENT_CONTAINER_STYLE = { paddingBottom : 24 } as const ;
148145
149146// Fallback map center when PLACES is empty (Madrid).
150147const FALLBACK_CENTER : Position = [ - 3.7038 , 40.4168 ] ;
@@ -324,7 +321,7 @@ export default function MapScreen() {
324321 // Drives the drag-synchronised top spacer inside the list header so the
325322 // first row slides smoothly down as the sheet expands behind the search
326323 // pill, matching Google Maps UX.
327- const sheetAnimatedIndex = useSharedValue ( SHEET_INDEX_LIST ) ;
324+ const sheetAnimatedIndex = useSharedValue ( SHEET_INDEX_MID ) ;
328325
329326 // Vertical space occupied by the floating search pill (safe-area top +
330327 // pill top margin + pill height + breathing room). The sheet is free to
@@ -339,22 +336,15 @@ export default function MapScreen() {
339336 PLACES [ 0 ] ?. id ?? "" ,
340337 ) ;
341338 const [ sheetMode , setSheetMode ] = useState < SheetMode > ( "list" ) ;
342- const [ sheetIndex , setSheetIndex ] = useState < number > ( SHEET_INDEX_LIST ) ;
339+ const [ sheetIndex , setSheetIndex ] = useState < number > ( SHEET_INDEX_MID ) ;
343340 const [ hasLocationPermission , setHasLocationPermission ] = useState ( false ) ;
344341 const [ userLocation , setUserLocation ] = useState < UserCoords | null > ( null ) ;
345342
346- // Tapping the search field expands the sheet to its top snap so the
347- // filtered list fills the screen. `KeyboardProvider` at the root
348- // layout keeps the keyboard in sync with the focused TextInput across
349- // bottom-sheet snap animations, so we can snap synchronously without
350- // Android losing focus a beat later.
351- //
352- // Intentionally no `onBlur` handler: re-snapping the sheet on blur
353- // triggers another keyboard lifecycle event which blurs the TextInput
354- // again, creating a focus-blur oscillation. The user can drag the
355- // sheet back down themselves if they want.
343+ // No `onBlur` handler: re-snapping on blur triggers another keyboard
344+ // lifecycle event which blurs the TextInput again, creating a
345+ // focus-blur oscillation.
356346 const handleSearchFocus = useCallback ( ( ) => {
357- sheetRef . current ?. snapToIndex ( SHEET_INDEX_SEARCH ) ;
347+ sheetRef . current ?. snapToIndex ( SHEET_INDEX_TOP ) ;
358348 } , [ ] ) ;
359349
360350 const handleCategoryFilter = useCallback ( ( next : CategoryFilter ) => {
@@ -429,7 +419,7 @@ export default function MapScreen() {
429419 hapticSelection ( ) ;
430420 setSelectedPlaceId ( place . id ) ;
431421 setSheetMode ( "detail" ) ;
432- sheetRef . current ?. snapToIndex ( SHEET_INDEX_DETAIL ) ;
422+ sheetRef . current ?. snapToIndex ( SHEET_INDEX_MID ) ;
433423 if ( shouldAnimateCamera ) {
434424 animateToCoordinate ( place . latitude , place . longitude ) ;
435425 }
@@ -455,26 +445,17 @@ export default function MapScreen() {
455445 } , [ ] ) ;
456446
457447 const handleSheetChange = useCallback ( ( index : number ) => {
458- setSheetIndex ( index ) ;
448+ setSheetIndex ( ( prev ) => ( prev === index ? prev : index ) ) ;
459449 } , [ ] ) ;
460450
461- // List content style — constant `paddingBottom` only; the top padding is
462- // driven per-frame by the animated header spacer below so the transition
463- // stays synchronised with the user's drag instead of snapping at the
464- // end of the animation.
465- const listContentContainerStyle = useMemo (
466- ( ) => ( { paddingBottom : 24 } ) ,
467- [ ] ,
468- ) ;
469-
470451 // Animated height for the spacer that sits at the very top of the list
471452 // header. Grows from 0 → sheetTopInset as the sheet's snap index moves
472453 // from mid (1) toward expanded (2), so the first visible row slides
473454 // down in lockstep with the sheet clearing the search pill.
474455 const headerSpacerStyle = useAnimatedStyle ( ( ) => ( {
475456 height : interpolate (
476457 sheetAnimatedIndex . value ,
477- [ SHEET_INDEX_LIST , SHEET_INDEX_SEARCH ] ,
458+ [ SHEET_INDEX_MID , SHEET_INDEX_TOP ] ,
478459 [ 0 , sheetTopInset ] ,
479460 Extrapolation . CLAMP ,
480461 ) ,
@@ -642,7 +623,7 @@ export default function MapScreen() {
642623 // flush with the pill) reads as grime; a border separates the pill from
643624 // the sheet cleanly.
644625 const pillSurfaceStyle = useMemo ( ( ) => {
645- if ( sheetIndex >= SHEET_INDEX_SEARCH ) {
626+ if ( sheetIndex >= SHEET_INDEX_TOP ) {
646627 return {
647628 borderWidth : StyleSheet . hairlineWidth ,
648629 borderColor : theme . colors . border ,
@@ -813,7 +794,7 @@ export default function MapScreen() {
813794 renderScrollComponent = { BottomSheetFlashListScrollable }
814795 data = { placesWithDistance }
815796 keyExtractor = { ( item ) => item . place . id }
816- contentContainerStyle = { listContentContainerStyle }
797+ contentContainerStyle = { LIST_CONTENT_CONTAINER_STYLE }
817798 ListHeaderComponent = {
818799 < View >
819800 < Animated . View style = { headerSpacerStyle } />
@@ -939,7 +920,12 @@ interface PlaceRowProps {
939920 onPress : ( ) => void ;
940921}
941922
942- function PlaceRow ( { place, distanceKm : km , isSelected, onPress } : PlaceRowProps ) {
923+ const PlaceRow = memo ( function PlaceRow ( {
924+ place,
925+ distanceKm : km ,
926+ isSelected,
927+ onPress,
928+ } : PlaceRowProps ) {
943929 const theme = useTheme ( ) ;
944930 const icon = CATEGORY_ICON [ place . category ] ;
945931 const distanceLabel = formatDistanceLabel ( km ) ;
@@ -1021,7 +1007,7 @@ function PlaceRow({ place, distanceKm: km, isSelected, onPress }: PlaceRowProps)
10211007 </ View >
10221008 </ Pressable >
10231009 ) ;
1024- }
1010+ } ) ;
10251011
10261012// ---------------------------------------------------------------------------
10271013// PlaceDetail — Google Maps inspired place sheet: hero image, title block,
0 commit comments