-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #181 from VKCOM/pulltorefresh
PullToRefresh component
- Loading branch information
Showing
9 changed files
with
562 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
.PullToRefresh { | ||
height: 100%; | ||
} | ||
|
||
.PullToRefresh--refreshing { | ||
touch-action: none; | ||
pointer-events: none; | ||
} | ||
|
||
.PullToRefresh__controls { | ||
left: 0; | ||
width: 100%; | ||
pointer-events: none; | ||
z-index: 100; | ||
} | ||
|
||
.PullToRefresh--ios .PullToRefresh__controls { | ||
z-index: 0; | ||
} | ||
|
||
.PullToRefresh__spinner { | ||
display: flex; | ||
margin: auto; | ||
width: 40px; | ||
height: 40px; | ||
align-items: center; | ||
justify-content: center; | ||
box-sizing: border-box; | ||
transition: transform 300ms cubic-bezier(.1, 0, .25, 1), opacity 220ms ease-out; | ||
opacity: 0; | ||
} | ||
|
||
.PullToRefresh--ios .PullToRefresh__spinner { | ||
color: var(--activity_indicator_tint); | ||
} | ||
|
||
.PullToRefresh--android .PullToRefresh__spinner { | ||
background: var(--background_suggestions); | ||
border-radius: 50%; | ||
box-shadow: 0 2px 6px rgba(0, 0, 0, .2); | ||
color: var(--accent); | ||
} | ||
|
||
.PullToRefresh--watching .PullToRefresh__spinner { | ||
transition: opacity 220ms ease-out; | ||
} | ||
|
||
.PullToRefresh__spinner-self { | ||
stroke: currentColor; | ||
} | ||
|
||
.PullToRefresh--refreshing .PullToRefresh__spinner-self { | ||
animation: PullToRefreshToRefreshing 380ms ease-out; | ||
} | ||
|
||
.PullToRefresh__spinner-path { | ||
transform: rotate(-90deg); | ||
transform-origin: center center; | ||
transition: stroke-dashoffset 167ms ease-out; | ||
} | ||
|
||
.PullToRefresh--watching .PullToRefresh__spinner-path, | ||
.PullToRefresh--refreshing .PullToRefresh__spinner-path { | ||
transition: none; | ||
} | ||
|
||
.PullToRefresh__spinner--on .PullToRefresh__spinner-path { | ||
animation: rotator var(--duration) linear infinite; | ||
} | ||
|
||
.PullToRefresh__content { | ||
overflow: hidden; | ||
transition: transform 400ms var(--ios-easing); | ||
} | ||
|
||
.PullToRefresh--watching .PullToRefresh__content { | ||
transition: none; | ||
} | ||
|
||
@keyframes PullToRefreshToRefreshing { | ||
0% { | ||
transform: scale(1); | ||
} | ||
|
||
30% { | ||
transform: scale(.6); | ||
} | ||
|
||
90% { | ||
transform: scale(1.1); | ||
} | ||
|
||
100% { | ||
transform: scale(1); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,240 @@ | ||
import React, { PureComponent } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import Touch from '../Touch/Touch'; | ||
import FixedLayout from '../FixedLayout/FixedLayout'; | ||
import classNames from '../../lib/classNames'; | ||
import { platform, ANDROID, IOS } from '../../lib/platform'; | ||
import getClassName from '../../helpers/getClassName'; | ||
import PullToRefreshSpinner from './PullToRefreshSpinner'; | ||
|
||
const baseClassName = getClassName('PullToRefresh'); | ||
|
||
const osname = platform(); | ||
const isAndroid = osname === ANDROID; | ||
const isIOS = osname === IOS; | ||
|
||
function cancelEvent (event) { | ||
if (!event) return false; | ||
while (event.originalEvent) event = event.originalEvent; | ||
if (event.preventDefault) event.preventDefault(); | ||
if (event.stopPropagation) event.stopPropagation(); | ||
if (event.stopImmediatePropagation) event.stopImmediatePropagation(); | ||
event.cancelBubble = true; | ||
event.returnValue = false; | ||
return false; | ||
} | ||
|
||
export default class PullToRefresh extends PureComponent { | ||
constructor (props) { | ||
super(props); | ||
|
||
this.params = { | ||
start: isAndroid ? -40 : -10, | ||
max: isAndroid ? 80 : 50, | ||
maxY: isAndroid ? 80 : 400, | ||
refreshing: isAndroid ? 50 : 36, | ||
|
||
positionMultiplier: isAndroid ? 1 : 0.21 | ||
}; | ||
|
||
this.state = { | ||
watching: false, | ||
refreshing: false, | ||
canRefresh: false, | ||
|
||
touchDown: false, | ||
refreshingFinished: false, | ||
|
||
touchY: 0, | ||
spinnerY: this.params.start, | ||
spinnerProgress: 0, | ||
contentShift: 0 | ||
}; | ||
|
||
this._contentElement = React.createRef(); | ||
} | ||
|
||
static propTypes = { | ||
children: PropTypes.element, | ||
className: PropTypes.string, | ||
|
||
/** | ||
* Будет вызвана для обновления контента | ||
*/ | ||
onRefresh: PropTypes.func.isRequired, | ||
|
||
/** | ||
* Определяет, выполняется ли обновление. Для скрытия спиннера после получения контента необходимо передать `false` | ||
*/ | ||
isFetching: PropTypes.bool | ||
}; | ||
|
||
static contextTypes = { | ||
window: PropTypes.any, | ||
document: PropTypes.any | ||
}; | ||
|
||
get document () { | ||
return this.context.document || document; | ||
} | ||
|
||
get window () { | ||
return this.context.window || window; | ||
} | ||
|
||
componentDidMount () { | ||
this.document.addEventListener('touchmove', this.onWindowTouchMove, { cancelable: true, passive: false }); | ||
} | ||
|
||
componentWillUnmount () { | ||
this.document.removeEventListener('touchmove', this.onWindowTouchMove); | ||
} | ||
|
||
componentWillReceiveProps (nextProps) { | ||
if (!nextProps.isFetching && this.props.isFetching) { | ||
this.onRefreshingFinish(); | ||
} | ||
} | ||
|
||
get scrollTop () { | ||
return this.document.scrollingElement.scrollTop; | ||
} | ||
|
||
onTouchStart = (e) => { | ||
if (this.state.refreshing) cancelEvent(e); | ||
this.setState({ touchDown: true }); | ||
}; | ||
|
||
onWindowTouchMove = (e) => { | ||
if (this.state.refreshing) cancelEvent(e); | ||
}; | ||
|
||
onTouchMove = (e) => { | ||
const { isY, shiftY } = e; | ||
const { start, max } = this.params; | ||
const pageYOffset = this.window.pageYOffset; | ||
|
||
const { refreshing, watching, touchDown } = this.state; | ||
|
||
if (watching && touchDown) { | ||
cancelEvent(e); | ||
|
||
const { positionMultiplier } = this.params; | ||
|
||
const shift = Math.max(0, shiftY - this.state.touchY); | ||
|
||
const currentY = Math.max(start, Math.min(this.params.maxY, start + (shift * positionMultiplier))); | ||
const progress = currentY > -10 ? Math.abs((currentY + 10) / max) * 80 : 0; | ||
|
||
this.setState({ | ||
spinnerY: currentY, | ||
spinnerProgress: Math.min(80, Math.max(0, progress)), | ||
canRefresh: progress > 80, | ||
contentShift: (currentY + 10) * 2.3 | ||
}); | ||
|
||
if (progress > 85 && !refreshing) { | ||
this.runRefreshing(); | ||
} | ||
} else if (isY && pageYOffset === 0 && shiftY > 0 && !refreshing && touchDown) { | ||
cancelEvent(e); | ||
|
||
this.setState({ | ||
watching: true, | ||
touchY: shiftY, | ||
spinnerY: start, | ||
spinnerProgress: 0 | ||
}); | ||
} | ||
}; | ||
|
||
onTouchEnd = () => { | ||
const { refreshing, canRefresh, refreshingFinished } = this.state; | ||
|
||
this.setState({ | ||
watching: false, | ||
touchDown: false | ||
}, () => { | ||
if (canRefresh && !refreshing) { | ||
this.runRefreshing(); | ||
} else if (refreshing && refreshingFinished) { | ||
this.resetRefreshingState(); | ||
} else { | ||
this.setState({ | ||
spinnerY: refreshing ? this.params.refreshing : this.params.start, | ||
spinnerProgress: 0, | ||
contentShift: 0 | ||
}); | ||
} | ||
}); | ||
}; | ||
|
||
runRefreshing () { | ||
if (!this.state.refreshing && this.props.onRefresh) { | ||
this.setState({ | ||
refreshing: true | ||
}); | ||
|
||
this.props.onRefresh(); | ||
} | ||
} | ||
|
||
onRefreshingFinish = () => { | ||
this.setState({ | ||
refreshingFinished: true | ||
}, () => { | ||
!this.state.touchDown && this.resetRefreshingState(); | ||
}); | ||
} | ||
|
||
resetRefreshingState () { | ||
this.setState({ | ||
watching: false, | ||
canRefresh: false, | ||
refreshing: false, | ||
refreshingFinished: false, | ||
spinnerY: this.params.start, | ||
spinnerProgress: 0, | ||
contentShift: 0 | ||
}); | ||
} | ||
|
||
render () { | ||
const { children, className, onRefresh, isFetching, ...restProps } = this.props; | ||
const { watching, refreshing, spinnerY, spinnerProgress, canRefresh, touchDown, contentShift } = this.state; | ||
|
||
return ( | ||
<Touch | ||
onStart={this.onTouchStart} | ||
onMove={this.onTouchMove} | ||
onEnd={this.onTouchEnd} | ||
className={classNames(baseClassName, className, { | ||
'PullToRefresh--watching': watching, | ||
'PullToRefresh--refreshing': refreshing | ||
})} | ||
{...restProps} | ||
> | ||
<FixedLayout className="PullToRefresh__controls"> | ||
<PullToRefreshSpinner | ||
style={{ | ||
transform: `translate3d(0, ${spinnerY}px, 0)`, | ||
opacity: watching || refreshing || canRefresh ? 1 : 0 | ||
}} | ||
on={refreshing} | ||
progress={refreshing ? null : spinnerProgress} | ||
/> | ||
</FixedLayout> | ||
|
||
<div | ||
className="PullToRefresh__content" | ||
ref={this._contentElement} | ||
style={{ | ||
transform: refreshing && !touchDown && isIOS ? `translate3d(0, 100px, 0)` : isIOS && contentShift ? `translate3d(0, ${contentShift}px, 0)` : '' | ||
}} | ||
> | ||
{children} | ||
</div> | ||
</Touch> | ||
); | ||
} | ||
} |
Oops, something went wrong.