ionic-team / ionic-team/ionic-framework
feat: Ability to locate and animate content relative to the keyboard
- Ngôn ngữ chính
- TypeScript
- Star
- 52.7k
- Fork
- 13.3k
- Merge trung bình
- 1 ngày 15 giờ
- Pull request đã merge (30 ngày)
- 51
Mô tả
### Prerequisites
- [X] I have read the [Contributing Guidelines](https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#creating-an-issue).
- [X] I agree to follow the [Code of Conduct](https://ionicframework.com/code-of-conduct).
- [X] I have searched for [existing issues](https://github.com/ionic-team/ionic-framework/issues) that already include this feature request, without success.
### Describe the Feature Request
It would be great if ionic handled animating the ion-footer (or ion-toolbar) on mobile when the keyboard opens & closes.
The implementation could look something like this:
https://github.com/ionic-team/ionic-framework/assets/41165256/6103fc21-1988-488e-bcbc-4d4123621fd1
With additional animations when the keyboard height changes while open like this:
https://github.com/ionic-team/ionic-framework/assets/41165256/3540978e-86c7-4936-bcc2-6e8f44570029
Additionally, there could be options for also resizing the ion-content's inner scrollable container.
This could be a new keyboard resize mode potentially?
### Describe the Use Case
This would provide a vastly better UX when ion-footer's & keyboards together.
### Describe Preferred Solution
_No response_
### Describe Alternatives
I've built my own solution to this, but it's far from perfect and was way too time-consuming to be viable as a general solution to this.
Some issues still remain like, if I navigate to a different page, if a dont let the ion-footer animation complete before the page transitions, the ion-footer on the next page is in the wrong place... I'm sure there's a better under-the-hood solution to make the animation work better with the navigation animation.
### Related Code
here's an example of the service I've made for this animation:
```
@Injectable({
providedIn: 'root',
})
export class ChatPageFooterAnimationSvc {
/**
* tracks the height of the keyboard when it was last opened
*/
onUpHeight: number;
/**
* tracks the current height of the keyboard. useful for comparing
* with onUpHeight to see if the keyboard height has changed
* (e.g. if user switches from emoji keyboard to regular keyboard)
*/
currentKeyboardHeight: number;
footer: Animation;
content: Animation;
ionContentHeight = document.body.clientHeight - 64 - 64;
ionToolbarEl: Element;
ionContentScrollEl: Element;
private stop = new Subject();
constructor(
private deviceSvc: DeviceSvc,
private keyboardSvc: KeyboardSvc,
private animationSvc: AnimationController,
private chatPageContentScrollSvc: ChatPageContentScrollSvc,
) {
onUnload(this.destroy);
// listens for when the keyboard is opened & triggers onUp animation
this.keyboardSvc.willShow$.pipe(
takeUntil(this.stop),
// willShow$ is emitted both when keyboard first appears, and when an opened keyboard changes height.
// so to tell if this is an event that should be handled by onUp(), we can check if a previous onUp()
// call has already set a truthy value for onUpHeight. If it has, then we know that the keyboard is
// currently visible & so this willShow event should NOT be handled by onUp()
filter(() => !this.onUpHeight),
map(event => correctKeyboardHeight(event.keyboardHeight, this.deviceSvc.model)),
).subscribe(async keyboardHeight =>
this.onUp(keyboardHeight)
);
// listens for when an opened keyboard changes height & triggers onChange styling
this.keyboardSvc.willShow$.pipe(
takeUntil(this.stop),
// willShow$ is emitted both when keyboard first appears, and when an opened keyboard changes height.
// so to tell if this is an event that should be handled by onChange(), we can check if the onUp()
// function has already set a value for onUpHeight. If it has, then we know that the keyboard is
// currently visible & so this willShow event should be handled by onChange()
filter(()=> !!this.onUpHeight),
map(event => correctKeyboardHeight(event.keyboardHeight, this.deviceSvc.model)),
).subscribe(async keyboardHeight =>
this.onChange(keyboardHeight)
)
// listens for when the keyboard is closed & triggers onDown animation
this.keyboardSvc.willHide$.pipe(
takeUntil(this.stop)
).subscribe(async () =>
this.onDown()
);
}
/**
* initializes some pre-reqs for the animation to function:
* @note the Keyboard MUST be set to KeyboardResize 'None' mode, otherwise:
* 1. animation measurements will be off (e.g. translateY's document reference position will change
* if KeyboardResize resizes the webview ('Native' mode) or the document ('Body' mode)...).
* 2. animations will be less smooth, b/c all the other KeyboardResize modes resize many more elements
* in the DOM tree - whereas with 'None' mode, we're only resizing the 2 elements passed to this service.
*/
init = async (): Promise => {
if (isMobile()) this.keyboardSvc.setResizeMode(KeyboardResize.None);
}
/**
* takes in the to-be-animated elements on the chatPage
*/
animateElement = async (el: Element): Promise => {
if (isWeb()) return;
if (isIonContentScrollEl(el)) this.ionContentScrollEl = el;
else this.ionToolbarEl = el;
}
/**
* preps the animation for the ionToolbarEl
*/
private createFooterAnimation = async (keyboardHeight: number): Promise => {
this.footer?.destroy();
this.footer = this.animationSvc
.create()
.duration(CHAT_PAGE_KEYBOARD_FOOTER_ANIMATION_DURATION_MS)
.iterations(1)
.easing('cubic-bezier(.43,1.1,.69,.94)')
.addElement(this.ionToolbarEl)
.from('transform', `translateY(0)`)
.to('transform', `translateY(-${keyboardHeight}px)`);
}
/**
* preps the animation for the ionContentScrollEl
*/
private createContentAnimation = async (keyboardHeight: number): Promise => {
this.content?.destroy();
this.content = this.animationSvc
.create()
.duration(250)
.iterations(1)
.easing('cubic-bezier(.43,1.1,.69,.94)')
.addElement(this.ionContentScrollEl)
.beforeStyles({height: `${this.ionContentHeight}px`, position: 'fixed', top: `${CHAT_PAGE_HEADER_HEIGHT_PX}px`})
.afterStyles({height: `${this.ionContentHeight - keyboardHeight}px`, position: 'fixed', top: `${CHAT_PAGE_HEADER_HEIGHT_PX}px`})
.from('transform', `translateY(0)`);
}
private createAnimations = async (keyboardHeight: number): Promise => {
await this.createContentAnimation(keyboardHeight);
await this.createFooterAnimation(keyboardHeight);
};
private onUp = async (keyboardHeight: number): Promise => {
// create animations must occur here, b/c this is the earliest we can get the keyboardHeight
await this.createAnimations(keyboardHeight);
// play animations in forward direction
this.footer.direction('normal').play({sync:false});
await this.content.direction('normal').play({sync:false});
// must scroll to bottom after animation is complete, else the bottom-most replies will be hidden
this.chatPageContentScrollSvc.scrollToBottom();
// set heights for onChange() & onDown() to leverage
[this.onUpHeight, this.currentKeyboardHeight] = [keyboardHeight, keyboardHeight];
}
private onDown = async (): Promise => {
// play animations in reverse direction
await this.footer.direction('reverse').beforeClearStyles(['position', 'bottom']).afterClearStyles(['position', 'bottom'])
.play({sync:true});
await this.content.direction('reverse').beforeClearStyles(['height', 'position', 'top']).afterClearStyles(['height', 'position', 'top'])
.play({sync:true});
// remove any style added by onChange()
this.ionToolbarEl.removeAttribute('style');
this.ionContentScrollEl.removeAttribute('style');
// must scroll to bottom after animation is complete, else the bottom-most replies will be hidden
this.chatPageContentScrollSvc.scrollToBottom();
// reset heights
[this.onUpHeight, this.currentKeyboardHeight] = [0, 0];
}
/**
* handles minor changes in the keyboard height when it's already opened
* @example when user switches from emoji keyboard to regular keyboard, its height changes
* @example when user switches from english keyboard to spanish keyboard, its height changes
*/
private onChange = async (keyboardHeight: number): Promise => {
this.currentKeyboardHeight = keyboardHeight;
this.ionContentScrollEl.setAttribute('style', `height: ${this.ionContentHeight - this.currentKeyboardHeight}px;`);
this.ionToolbarEl.setAttribute('style', `bottom: ${this.currentKeyboardHeight - this.onUpHeight}px;`);
}
/**
* resets the service state to it's inital state, so when/if the chatPage is re-entered,
* the page elements will be resubmited so the animation can be created using the new elements
*/
reset = (): void => this.destroyAnimations();
/**
* sometimes if you leave the chatPage with the animations still running,
* they'll cause some jankiness when you re-enter the chatPage. By stopping
* them before you leave the page, we avoid such jank.
*/
stopAnimations = async (): Promise => {
if (await firstValueFrom(this.keyboardSvc.isHidden$)) return;
this.footer.stop(), this.content.stop();
await delay(50)
await this.keyboardSvc.hide();
}
private destroy = (): void => (this.stop.next(), this.stop.complete(), this.destroyAnimations())
private destroyAnimations = (): void => (this.footer?.destroy(), this.content?.destroy());
}
```
Hướng dẫn đóng góp
Đánh giá
Issue này chưa được đánh giá.