taskswitcher: Gesture Navigation: Quality of Life Improvements

Most Notable Changes Here Include:
1. If the user moves and lifts their finger up halfway up the screen, the navigation gesture will now go home.
2. The task drawer will now move in and out of view depending on the gesture navigation state.
3. The app window will now continue to shrink with resistance as the window moves further up.
4. When in the task drawer, if the user drags up from the bottom, the current task will now follow the users finger like the task does when dragging up from within an app.
5. The task drawer will now slide in from the side when it is not within an app.

I would upload a video here to showcase these changes. However, I was unable to get OBS to record my screen while in my plasma mobile session.
This commit is contained in:
Micah Stanley 2024-10-15 00:55:29 +00:00 committed by Devin Lin
parent 971879f10f
commit fdc8958ce5
5 changed files with 523 additions and 157 deletions

View file

@ -25,6 +25,9 @@ Flickable {
// update position from horizontal flickable movement // update position from horizontal flickable movement
property real oldContentX property real oldContentX
onContentXChanged: { onContentXChanged: {
// disable if animations are running to prevent bugs
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
if (moving) { if (moving) {
// TODO whenever flicking actually works this should probably be swapped with // TODO whenever flicking actually works this should probably be swapped with
// a minimum velocity after which it should snap to the nearest task // a minimum velocity after which it should snap to the nearest task
@ -35,14 +38,17 @@ Flickable {
} }
onMovementStarted: { onMovementStarted: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
taskSwitcherHelpers.cancelAnimations(); taskSwitcherHelpers.cancelAnimations();
} }
onMovementEnded: { onMovementEnded: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight); taskSwitcherHelpers.snapToNearestTaskWorkaround(movingRight);
resetPosition(); resetPosition();
} }
onFlickStarted: { onFlickStarted: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
root.cancelFlick(); root.cancelFlick();
} }
onFlickEnded: { onFlickEnded: {
@ -51,6 +57,7 @@ Flickable {
} }
onDraggingChanged: { onDraggingChanged: {
if (taskSwitcherHelpers.currentlyBeingClosed) {return}
if (dragging) { if (dragging) {
taskSwitcherHelpers.cancelAnimations(); taskSwitcherHelpers.cancelAnimations();
} else { } else {

View file

@ -25,6 +25,8 @@ Item {
readonly property real dragOffset: -control.y readonly property real dragOffset: -control.y
readonly property int currentIndex: model.index
// whether this task is being interacted with // whether this task is being interacted with
readonly property bool interactingActive: control.pressed && control.passedDragThreshold readonly property bool interactingActive: control.pressed && control.passedDragThreshold
@ -136,6 +138,7 @@ Item {
to: 0 to: 0
onFinished: { onFinished: {
if (to != 0) { // close app if (to != 0) { // close app
taskSwitcherHelpers.lastClosedTask = currentIndex;
delegate.closeApp(); delegate.closeApp();
uncloseTimer.start(); uncloseTimer.start();
} }
@ -182,7 +185,10 @@ Item {
icon.name: "window-close" icon.name: "window-close"
icon.width: Kirigami.Units.iconSizes.smallMedium icon.width: Kirigami.Units.iconSizes.smallMedium
icon.height: Kirigami.Units.iconSizes.smallMedium icon.height: Kirigami.Units.iconSizes.smallMedium
onClicked: delegate.closeApp() onClicked: {
taskSwitcherHelpers.lastClosedTask = currentIndex;
delegate.closeApp()
}
} }
} }

View file

@ -23,31 +23,22 @@ MouseArea {
property int taskInteractingCount: 0 property int taskInteractingCount: 0
// account for system header and footer offset (center the preview image) // account for system header and footer offset (center the preview image)
readonly property real taskY: { readonly property real taskYBase: {
let headerHeight = shellTopMargin; let headerHeight = shellTopMargin;
let footerHeight = shellBottomMargin; let footerHeight = shellBottomMargin;
let diff = headerHeight - footerHeight; let diff = headerHeight - footerHeight;
let baseY = (taskSwitcher.height / 2) - (taskSwitcherHelpers.taskHeight / 2) - (taskSwitcherHelpers.taskHeaderHeight / 2); let baseY = (taskSwitcher.height / 2) - (taskSwitcherHelpers.taskHeight / 2) - (taskSwitcherHelpers.taskHeaderHeight / 2);
return baseY + diff / 2 - shellTopMargin - trackFingerYOffset; return baseY + diff / 2 - shellTopMargin;
} }
readonly property real trackFingerYOffset: { readonly property real taskY: {
if (taskSwitcherHelpers.isScaleClamped) { let trackFingerYOffsetClamped = 0;
let openedPos = taskSwitcherHelpers.openedYPosition; if (taskSwitcherHelpers.isScaleClamped && (taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened)) {
let directTrackingOffset = openedPos * 0.2 trackFingerYOffsetClamped = taskSwitcherHelpers.trackFingerYOffset;
if (taskSwitcherState.yPosition < openedPos + directTrackingOffset) {
// Allow the task list to move further up than the fully opened position
return taskSwitcherState.yPosition - openedPos;
} else {
// but make it more reluctant the further up it goes
let overDragProgress = (taskSwitcherState.yPosition - directTrackingOffset - openedPos) / openedPos;
// Base formula is 1-2.3^(-progress) which asymptotically approaches 1
return (1 - Math.pow(2.3, -overDragProgress)) * openedPos + directTrackingOffset;
}
} else {
return 0;
} }
return taskYBase - trackFingerYOffsetClamped;
} }
function getTaskAt(index) { function getTaskAt(index) {
@ -82,6 +73,59 @@ MouseArea {
} }
} }
// the postition offset value for non-active tasks in the task drawer
// this value is normalized and is usually set to 0, 1, or 2 (larger the number, the further they are from the active task)
property real baseTaskOffset: 0
// the position offset value tracked to the touch x position when opening the task drawer from the home screen
// this allows the task switcher to move left and right without causing problems with the task offset animations
property real homeTouchPositionX: 0
// the touch x position value normalized between 0 and 1
// the base value should be 0.5 for when it is on the home screen
readonly property real touchPosition: {
let value = 0.5
if (taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened) {
// since the touch position starts at 0, we add half the window width and then divide it by the full width to normalize it
value = ((taskSwitcherHelpers.notHomeScreenState ? taskSwitcherState.touchXPosition : 0) + (taskSwitcherHelpers.windowWidth / 2)) / taskSwitcherHelpers.windowWidth
value = Math.min(1, Math.max(0, value))
}
return value
}
// dynamic task offset animation duration based off of the touch position and task scale
function dynamicDuration(left = true) {
// if the close animation is running, use the standard long duration time for consistency
let duration = Kirigami.Units.longDuration * 1.75
if (!taskSwitcherHelpers.closeAnim.running && taskSwitcherHelpers.notHomeScreenState && taskSwitcherHelpers.gestureState != TaskSwitcherHelpers.GestureStates.HorizontalSwipe && !taskSwitcherHelpers.isInTaskScrubMode) {
// max out the scale at 1 so it is not too fast when opening the task drawer with the button
let taskScale = Math.min(taskSwitcherHelpers.currentScale, 1)
// change the duration based off of the touch position and task scale
duration = duration * ((left ? touchPosition : (1 - touchPosition)) + 1)
}
return duration
}
// the duration is set to 0 if setOffsetDurationImmediately is true so we can skip the animation
readonly property real taskOffsetDurationLeft: setOffsetDurationImmediately ? 0 : dynamicDuration(true)
readonly property real taskOffsetDurationRight: setOffsetDurationImmediately ? 0 : dynamicDuration(false)
// the easing type for the task offset animation
property int taskOffsetEasing: Easing.InOutQuart
// skips the animation and sets the task offset value immediately (only should be set by 'setTaskOffsetValue')
property bool setOffsetDurationImmediately: true
// set the task offset value with an animation unless specified otherwise
function setTaskOffsetValue(value, immediately = false, taskEasing = ((taskSwitcherHelpers.notHomeScreenState || (value != 0)) && (baseTaskOffset != taskSwitcherHelpers.taskOffsetValue)) ? Easing.InOutQuart : Easing.OutQuart) {
if (baseTaskOffset == value && immediately) {
baseTaskOffset = value + 1;
}
setOffsetDurationImmediately = immediately;
taskOffsetEasing = taskEasing;
baseTaskOffset = value;
}
transform: Scale { transform: Scale {
origin.x: root.width / 2 origin.x: root.width / 2
origin.y: root.height / 2 origin.y: root.height / 2
@ -95,7 +139,8 @@ MouseArea {
} }
onPressedChanged: { onPressedChanged: {
if (!taskSwitcherState.currentlyBeingOpened && pressed) { // disable if being closed or opened to prevent bugs
if (!taskSwitcherHelpers.currentlyBeingOpened && !taskSwitcherHelpers.currentlyBeingClosed && pressed) {
// ensure animations aren't running when finger is pressed // ensure animations aren't running when finger is pressed
taskSwitcherHelpers.cancelAnimations(); taskSwitcherHelpers.cancelAnimations();
} }
@ -111,9 +156,10 @@ MouseArea {
delegate: Task { delegate: Task {
id: task id: task
readonly property int currentIndex: model.index readonly property int currentIndex: model.index
readonly property bool isCurrentTask: currentIndex == taskSwitcherHelpers.currentDisplayTask
// this is the x-position with respect to the list // this is the x-position with respect to the list
property real listX: taskSwitcherHelpers.xPositionFromTaskIndex(currentIndex); property real listX: taskSwitcherHelpers.xPositionFromTaskIndex(currentIndex)
Behavior on listX { Behavior on listX {
NumberAnimation { NumberAnimation {
duration: Kirigami.Units.longDuration duration: Kirigami.Units.longDuration
@ -121,23 +167,62 @@ MouseArea {
} }
} }
// the animated task offset value (always will be 0 if it is the current task in the task drawer)
property real taskOffsetNormalized: (baseTaskOffset * ((taskSwitcherHelpers.taskDrawerOpened && isCurrentTask) ? 0 : 1))
Behavior on taskOffsetNormalized {
NumberAnimation {
duration: taskSwitcherHelpers.currentDisplayTask > currentIndex ? taskOffsetDurationRight : taskOffsetDurationLeft
easing.type: taskOffsetEasing
easing.overshoot: 0.85
}
}
// calculate which direction to offset
readonly property real offsetDir: (taskSwitcherHelpers.currentDisplayTask > currentIndex ? -1 : 1)
// check if this task should be offset
readonly property real isOffScreenOffset: {
let isOffsetBase = ((!taskSwitcherState.wasInActiveTask && !taskSwitcherHelpers.taskDrawerOpened) || !isCurrentTask) ? (taskSwitcherHelpers.isInTaskScrubMode && taskSwitcherHelpers.notHomeScreenState ? 0 : 1) : 0
let isOffsetTaskDrawer = (currentIndex == taskSwitcherHelpers.currentDisplayTask ? 0 : 1)
return taskSwitcherHelpers.taskDrawerOpened ? isOffsetTaskDrawer : isOffsetBase
}
// how far the task needs to travel to be off screen
readonly property real scrollXOffset: Math.abs(taskSwitcherHelpers.xPositionFromTaskIndex(taskSwitcherHelpers.currentDisplayTask) - (taskSwitcherState.xPosition + (taskSwitcherState.touchXPosition / taskSwitcherHelpers.currentScale)))
readonly property real offScreenOffset: (taskSwitcherHelpers.windowWidth * (((taskSwitcherHelpers.notHomeScreenState ? taskSwitcherState.touchXPosition : 0) * offsetDir * ((homeTouchPositionX == 0) ? 1 : 0) + (taskSwitcherHelpers.windowWidth / 2)) / taskSwitcherHelpers.windowWidth));
// calculate the actual task offset
readonly property real taskOffset: ((offScreenOffset + (taskSwitcherHelpers.notHomeScreenState ? scrollXOffset : 0)) / taskSwitcherHelpers.currentScale - (homeTouchPositionX * (1 - Math.max(0, Math.min(1, (taskOffsetNormalized - taskSwitcherHelpers.peekOffsetValue) / (taskSwitcherHelpers.homeOffsetValue - taskSwitcherHelpers.peekOffsetValue)))))) * taskOffsetNormalized * isOffScreenOffset * offsetDir
// extra resistance calculated for non-current task in the task drawer
readonly property real nonCurrentScaleResistance: ((isCurrentTask && taskSwitcherHelpers.notHomeScreenState) || taskSwitcherHelpers.fromButton) ? 0 : 1 - Math.min(taskSwitcherHelpers.currentScale, 1)
readonly property real nonCurrentScaleXOffset: (isCurrentTask && taskSwitcherHelpers.notHomeScreenState) ? 0 : ((taskSwitcherHelpers.taskWidth) * (scale - 1) * (currentIndex - taskSwitcherHelpers.currentDisplayTask))
readonly property real nonCurrentXPositionResistance: (isCurrentTask && taskSwitcherHelpers.notHomeScreenState) ? 0 : (taskSwitcherHelpers.taskWidth * (scale - 1)) * (taskSwitcherHelpers.notHomeScreenState ? 0.25 : 1.0) * offsetDir
readonly property real nonCurrentYPositionResistance: (isCurrentTask && taskSwitcherHelpers.notHomeScreenState) ? 0 : ((taskSwitcher.height / 2)) * nonCurrentScaleResistance
// this is the actual displayed x-position on screen // this is the actual displayed x-position on screen
x: listX + repeater.leftMargin - taskSwitcherState.xPosition x: listX + repeater.leftMargin - taskSwitcherState.xPosition - taskOffset - nonCurrentScaleXOffset + nonCurrentXPositionResistance
y: root.taskY y: ((taskSwitcherState.wasInActiveTask || taskSwitcherHelpers.taskDrawerOpened) ? root.taskY + nonCurrentYPositionResistance * 0.5: root.taskY / (taskSwitcherHelpers.fromButton ? 1 : (1 + taskOffsetNormalized * 0.075))) // add more resistance when not the current task
scale: ((isCurrentTask && taskSwitcherHelpers.notHomeScreenState) || taskSwitcherHelpers.fromButton) ? 1 : (1 + nonCurrentScaleResistance) * (1 + taskOffsetNormalized * 0.075) // add more resistance when not the current task and resist even further if the task is offset
// ensure current task is above others // ensure current task is above others
z: taskSwitcherState.currentTaskIndex === currentIndex ? 1 : 0 z: isCurrentTask ? 1 : 0
// only show header once task switcher is opened // only show header once task switcher is opened
showHeader: !taskSwitcherState.gestureInProgress && !taskSwitcherHelpers.currentlyBeingClosed && !taskSwitcherHelpers.isInTaskScrubMode showHeader: !taskSwitcherState.gestureInProgress && !taskSwitcherHelpers.currentlyBeingClosed && !taskSwitcherHelpers.isInTaskScrubMode
// darken effect as task gets away from the center of the screen // darken effect as task gets away from the center of the screen
darken: { darken: {
const distFromCentreProgress = Math.abs(x - repeater.leftMargin) / taskSwitcherHelpers.taskWidth; const distFromCentreProgress = Math.abs(x - repeater.leftMargin - (taskSwitcherHelpers.currentlyBeingOpened || taskSwitcherHelpers.currentlyBeingClosed ? (taskSwitcherHelpers.xPositionFromTaskIndex(taskSwitcherHelpers.currentDisplayTask)) - taskSwitcherState.xPosition : 0)) / taskSwitcherHelpers.taskWidth;
const upperBoundAdjust = Math.min(0.5, distFromCentreProgress) - 0.2; const upperBoundAdjust = Math.min(0.25, distFromCentreProgress) - 0.2;
return Math.max(0, upperBoundAdjust); return Math.max(0, upperBoundAdjust);
} }
// fade out as the task closes
opacity: taskSwitcherHelpers.closingFactor
// update count of tasks being interacted with, so we know whether we are in a swipe up action // update count of tasks being interacted with, so we know whether we are in a swipe up action
onInteractingActiveChanged: { onInteractingActiveChanged: {
let offset = interactingActive ? 1 : -1; let offset = interactingActive ? 1 : -1;

View file

@ -53,12 +53,27 @@ FocusScope {
// keep track of task list events // keep track of task list events
property int oldTasksCount: tasksCount property int oldTasksCount: tasksCount
onTasksCountChanged: { onTasksCountChanged: {
// we need to subtract 1 from the current index when the closed task index is smaller
// this is because this part of the list has been shifted down by 1 when the closed task was removed.
if (taskSwitcherHelpers.lastClosedTask < state.currentTaskIndex) {
state.currentTaskIndex -= 1;
// animated at the same speed as the task x position in the TaskList so that the task appears not to move from the perspective of the user.
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex, Kirigami.Units.longDuration, Easing.InOutQuad);
taskSwitcherHelpers.lastClosedTask = -1;
}
if (tasksCount === 0 && oldTasksCount !== 0) { if (tasksCount === 0 && oldTasksCount !== 0) {
hide(); hide();
} else if (tasksCount < oldTasksCount && state.currentTaskIndex >= tasksCount) { } else if (tasksCount < oldTasksCount) {
if (state.currentTaskIndex < 0) {
// if the user is on the frist task, and it is closed, scroll right
taskSwitcherHelpers.animateGoToTaskIndex(0, Kirigami.Units.longDuration);
} else if (state.currentTaskIndex >= tasksCount) {
// if the user is on the last task, and it is closed, scroll left // if the user is on the last task, and it is closed, scroll left
taskSwitcherHelpers.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration); taskSwitcherHelpers.animateGoToTaskIndex(tasksCount - 1, Kirigami.Units.longDuration);
} }
}
oldTasksCount = tasksCount; oldTasksCount = tasksCount;
} }
@ -66,20 +81,60 @@ FocusScope {
Keys.onEscapePressed: hide(); Keys.onEscapePressed: hide();
Component.onCompleted: { Component.onCompleted: {
initialSetup();
}
function initialSetup() {
taskSwitcherHelpers.cancelAnimations();
state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow); state.updateWasInActiveTask(KWinComponents.Workspace.activeWindow);
// task index from last time using the switcher // ensure the task drawer is not opened and reset values to defaults
taskSwitcherHelpers.reachedHeightThreshold = false;
taskSwitcherHelpers.gestureState = TaskSwitcherHelpers.GestureStates.Undecided;
taskSwitcherHelpers.isInTaskScrubMode = false;
taskSwitcherHelpers.hasVibrated = false;
taskSwitcherHelpers.closingFactor = 1;
taskSwitcherHelpers.taskSwitchCanLaunch = false;
taskSwitchCanLaunchTimer.restart()
taskList.taskOffsetEasing = Easing.InOutQuart;
taskList.homeTouchPositionX = 0;
backgroundColorOpacityAn.enabled = false;
backgroundColorOpacity = state.wasInActiveTask ? 1 : 0;
backgroundColorOpacityAn.enabled = true;
// reset the offset to have the task drawer off screen
taskList.setTaskOffsetValue(state.wasInActiveTask ? taskSwitcherHelpers.taskOffsetValue : taskSwitcherHelpers.homeOffsetValue, true);
// task index from the last time using the switcher
state.initialTaskIndex = Math.min(state.currentTaskIndex, tasksCount - 1); state.initialTaskIndex = Math.min(state.currentTaskIndex, tasksCount - 1);
if (state.wasInActiveTask) { if (state.wasInActiveTask) {
// if we were in an active task instead set initial task index to the position of that task // if we were in an active task instead set initial task index to the position of that task
state.initialTaskIndex = taskSwitcherHelpers.getTaskIndexFromWindow(KWinComponents.Workspace.activeWindow); state.initialTaskIndex = taskSwitcherHelpers.getTaskIndexFromWindow(KWinComponents.Workspace.activeWindow);
} else {
// reset the task index to the start if on home screen
state.initialTaskIndex = 0
} }
state.currentTaskIndex = state.initialTaskIndex
taskSwitcherHelpers.currentDisplayTask = state.currentTaskIndex;
taskSwitcherHelpers.goToTaskIndex(state.initialTaskIndex); taskSwitcherHelpers.goToTaskIndex(state.initialTaskIndex);
taskList.minimizeAll(); taskList.minimizeAll();
// fully open the switcher (if this is a button press, not gesture) // fully open the switcher (if this is a button press, not gesture)
if (!root.state.gestureInProgress) { if (!root.state.gestureInProgress) {
taskSwitcherHelpers.fromButton = true;
if (state.wasInActiveTask) {
taskList.setTaskOffsetValue(0, true);
} else {
taskList.setTaskOffsetValue(0);
}
backgroundColorOpacity = 1;
taskSwitcherHelpers.open(); taskSwitcherHelpers.open();
} }
} }
@ -102,8 +157,9 @@ FocusScope {
// task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion // task scrub mode allows scrubbing through a number of tasks with a mostly horizontal motion
function taskScrubMode() { function taskScrubMode() {
taskList.setTaskOffsetValue(0, false, Easing.OutQuart);
if (!taskSwitcherHelpers.isInTaskScrubMode) { if (!taskSwitcherHelpers.isInTaskScrubMode) {
taskSwitcherHelpers.isInTaskScrubMode = true; backgroundColorOpacity = 1;
taskSwitcherHelpers.cancelAnimations(); taskSwitcherHelpers.cancelAnimations();
taskSwitcherHelpers.open(); taskSwitcherHelpers.open();
if (!taskSwitcherHelpers.hasVibrated) { if (!taskSwitcherHelpers.hasVibrated) {
@ -111,43 +167,140 @@ FocusScope {
haptics.buttonVibrate(); haptics.buttonVibrate();
taskSwitcherHelpers.hasVibrated = true; taskSwitcherHelpers.hasVibrated = true;
} }
} }
let newTaskIndex = Math.max(0, Math.min(tasksCount - 1, Math.floor(state.touchXPosition / taskSwitcherHelpers.taskScrubDistance) + state.initialTaskIndex)); // TODO this makes sense, but makes scrub mode feel a bit weird
if (newTaskIndex != state.currentTaskIndex) { // improve trigger distance logic for task scrub mode to fix
let newTaskIndex = Math.max(0, Math.min(tasksCount - 1, Math.floor(state.touchXPosition / taskSwitcherHelpers.taskScrubDistance) + state.initialTaskIndex - (state.wasInActiveTask ? 0 : 1)));
if (newTaskIndex != state.currentTaskIndex || !taskSwitcherHelpers.isInTaskScrubMode) {
taskSwitcherHelpers.animateGoToTaskIndex(newTaskIndex); taskSwitcherHelpers.animateGoToTaskIndex(newTaskIndex);
taskSwitcherHelpers.isInTaskScrubMode = true;
} }
} }
function onTouchPositionChanged() { function onTouchPositionChanged() {
let unmodifiedYposition = Math.abs(state.touchYPosition)
if (taskSwitcherHelpers.isInTaskScrubMode || // once in scrub mode, let's not allow to go out, that can result in inconsistent UX if (taskSwitcherHelpers.isInTaskScrubMode || // once in scrub mode, let's not allow to go out, that can result in inconsistent UX
(Math.abs(state.xVelocity) > Math.abs(state.yVelocity) * 3 && // gesture needs to be almost completely horizontal (Math.abs(state.xVelocity) > Math.abs(state.yVelocity) * 3 && // gesture needs to be almost completely horizontal
Math.abs(state.xVelocity) < 2.5 && // and not with a fast flick TODO! evaluate whether to keep this, it's kinda awkward Math.abs(state.xVelocity) < 2.5 && // and not with a fast flick TODO! evaluate whether to keep this, it's kinda awkward
Math.abs(state.touchXPosition) > taskSwitcherHelpers.taskScrubDistance * 0.95 && // and have moved far enough sideways Math.abs(state.touchXPosition) > taskSwitcherHelpers.taskScrubDistance * 0.95 && // and have moved far enough sideways
state.yPosition < taskSwitcherHelpers.undoYThreshold && // and be close to the screen edge unmodifiedYposition < Kirigami.Units.largeSpacing * 2 && // and be close to the screen edge
tasksCount > 1 // and there needs to be more than one task open tasksCount > 0 && // and there needs to be more than none task open
!taskSwitcherHelpers.taskDrawerOpened // and the task drawer must not be open
)) { )) {
taskScrubMode(); taskScrubMode();
} else { } else {
if (state.status == TaskSwitcherData.TaskSwitcherState.Active) { if (taskSwitcherHelpers.currentlyBeingClosed) {
// task switcher is already open // if the task switch is still open but playing the close animation
// TODO add some sort of feedback for dismissing task switcher (maybe opacity reduction?) // setup some values and return to the initial setup so that the user can always navigate with no down time
return; state.wasInActiveTask = taskSwitcherHelpers.openAppAnim.running ? true : false
taskList.setTaskOffsetValue(state.wasInActiveTask ? taskSwitcherHelpers.taskOffsetValue : taskSwitcherHelpers.homeOffsetValue, true);
state.status = !state.wasInActiveTask ? (taskSwitcherHelpers.openAppAnim.closeAnim && !taskSwitcherHelpers.taskDrawerWillOpen ? TaskSwitcherData.TaskSwitcherState.Active : TaskSwitcherData.TaskSwitcherState.Inactive) : TaskSwitcherData.TaskSwitcherState.Inactive
initialSetup();
} else if (taskSwitcherHelpers.openAnim.running) {
taskSwitcherHelpers.cancelAnimations();
state.status = taskSwitcherHelpers.stateClass.Active;
} }
state.yPosition = Math.abs(state.touchYPosition);
state.xPosition = taskSwitcherHelpers.xPositionFromTaskIndex(state.initialTaskIndex) - state.touchXPosition; state.yPosition = unmodifiedYposition + (taskSwitcherHelpers.taskDrawerOpened || !state.wasInActiveTask ? taskSwitcherHelpers.openedYPosition : 0);
let newXPosition = taskSwitcherHelpers.xPositionFromTaskIndex(state.initialTaskIndex);
if (taskSwitcherHelpers.notHomeScreenState && !taskSwitcherHelpers.currentlyBeingClosed) {
newXPosition = newXPosition - (state.touchXPosition / taskSwitcherHelpers.currentScale);
}
state.xPosition = newXPosition;
// allows the user to move the task drawer left and right when on the home screen
taskList.homeTouchPositionX = taskSwitcherHelpers.notHomeScreenState ? 0 : (state.touchXPosition * 0.35);
// dynamically update the task switcher state based off of the touch position and velocity
updateTaskSwitcherState()
} }
} }
// actions on an upwards flick function updateTaskSwitcherState() {
function upwardsFlick() { let unmodifiedYposition = Math.abs(state.touchYPosition)
if (state.wasInActiveTask) {
// go to homescreen if we were in an active task // if the touch is above heightThreshold, set reachedHeightThreshold to true
taskSwitcherHelpers.close(); if (unmodifiedYposition > taskSwitcherHelpers.heightThreshold) {
// set reachedHeightThreshold when above or below two separate points to helps prevent flickering when the task switcher moves in and out of view
taskSwitcherHelpers.reachedHeightThreshold = true;
backgroundColorOpacity = taskSwitcherHelpers.notHomeScreenState ? 0 : 1;
} else if (unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
backgroundColorOpacity = 1;
} else { } else {
// or normally open task switcher if we were on the homescreen already backgroundColorOpacity = taskSwitcherHelpers.notHomeScreenState ? 1 : 0;
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex); }
taskSwitcherHelpers.open();
if (state.totalSquaredVelocity > state.flickVelocityThreshold) {
// flick
// ratio between y and x velocity as threshold between vertical and horizontal flick
let xyVelocityRatio = 1.7; // with 1.7 swipes up to ~60° from horizontal are counted as horizontal
if (state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) {
// downwards flick
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided);
if (unmodifiedYposition < taskSwitcherHelpers.undoYThreshold) {
taskList.setTaskOffsetValue(taskSwitcherHelpers.notHomeScreenState ? 0 : taskSwitcherHelpers.homeOffsetValue);
}
} else if (-state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio || (taskSwitcherHelpers.reachedHeightThreshold && taskSwitcherHelpers.notHomeScreenState)) {
// upwards flick or if the touch is above heightThreshold
if (taskSwitcherHelpers.notHomeScreenState) {
// if in app or task switcher, go home
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
if (taskSwitcherHelpers.reachedHeightThreshold) {
taskList.setTaskOffsetValue(taskSwitcherHelpers.taskOffsetValue);
}
} else if (unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
// else, keep the task switcher in view
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(taskSwitcherHelpers.peekOffsetValue);
}
} else if (!taskSwitcherHelpers.reachedHeightThreshold && !taskSwitcherHelpers.isInTaskScrubMode) {
// sideways flick
if (taskSwitcherHelpers.notHomeScreenState) {
taskList.setTaskOffsetValue(0, unmodifiedYposition < taskSwitcherHelpers.openedYPosition ? true : false);
}
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.HorizontalSwipe);
}
} else {
if (unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
// if just moveing out of undoYThreshold, set the state to home
if (taskSwitcherHelpers.gestureState < TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home)
}
// if the touch is above heightThreshold, it will retrun home
if (unmodifiedYposition > taskSwitcherHelpers.heightThreshold) {
taskSwitcherHelpers.hasVibrated = true;
if (taskSwitcherHelpers.notHomeScreenState) {
// move the task switcher out of view
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Home);
taskList.setTaskOffsetValue(taskSwitcherHelpers.taskOffsetValue);
} else {
// keep the task switcher in view when above heightThreshold and from home
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(taskSwitcherHelpers.peekOffsetValue);
}
// minus largeSpacing from the heightThreshold to help prevent flickering when the task switcher moves in and out of view
} else if ((unmodifiedYposition < taskSwitcherHelpers.heightThreshold - Kirigami.Units.largeSpacing) || taskSwitcherHelpers.reachedHeightThreshold == false) {
// set reachedHeightThreshold when above or below two separate points to helps prevent flickering when the task switcher moves in and out of view
taskSwitcherHelpers.reachedHeightThreshold = false;
if (state.totalSquaredVelocity < state.flickVelocityThreshold && taskSwitcherHelpers.taskSwitchCanLaunch) {
// if velocity is small enough, move the task switcher into view
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher);
taskList.setTaskOffsetValue(taskSwitcherHelpers.notHomeScreenState ? 0 : taskSwitcherHelpers.peekOffsetValue);
}
}
} else {
// if under the undo threshold, it will go back to the task switcher if it is open
if (taskSwitcherHelpers.taskDrawerOpened) {
taskSwitcherHelpers.reachedHeightThreshold = false;
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.TaskSwitcher)
taskList.setTaskOffsetValue(0);
} else {
taskSwitcherHelpers.reachedHeightThreshold = false;
setTaskDrawerState(TaskSwitcherHelpers.GestureStates.Undecided)
taskList.setTaskOffsetValue(taskSwitcherHelpers.notHomeScreenState ? 0 : taskSwitcherHelpers.homeOffsetValue);
}
}
} }
} }
@ -162,26 +315,43 @@ FocusScope {
// diagonal quick switch gesture logic // diagonal quick switch gesture logic
function quickSwitch() { function quickSwitch() {
// should "quick switch" to adjacent app in task switcher, but only if we were in an app before // should "quick switch" to adjacent app in task switcher, but only if we were in an app before
let unmodifiedYposition = Math.abs(state.touchYPosition)
let newIndex = state.currentTaskIndex; let newIndex = state.currentTaskIndex;
let shouldSwitch = false; let shouldSwitch = false;
if (state.xVelocity > 0 && state.wasInActiveTask) { if (state.xVelocity > 0) {
// flick to the right, go to app to the left if (taskSwitcherHelpers.notHomeScreenState) {
// flick to the right, go to the app on the left
newIndex = state.currentTaskIndex + 1; newIndex = state.currentTaskIndex + 1;
}
if (newIndex < tasksCount) { if (newIndex < tasksCount) {
// switch only if flick doesn't go over end of list // switch only if flick doesn't go over end of list
shouldSwitch = true; shouldSwitch = true;
} }
} else if (state.xVelocity < 0 && state.wasInActiveTask) { } else if (state.xVelocity < 0) {
if (taskSwitcherHelpers.notHomeScreenState) {
// flick to the left, go to app to the right // flick to the left, go to app to the right
newIndex = state.currentTaskIndex - 1; newIndex = state.currentTaskIndex - 1;
if (newIndex >= 0) { if (newIndex >= 0) {
// switch only if flick doesn't go over end of list // switch only if flick doesn't go over end of list
shouldSwitch = true; shouldSwitch = true;
} }
} else {
// flick to the left on the home screen, dismiss the gesture
taskSwitcherHelpers.close();
retrun;
}
} }
if (shouldSwitch) { if (shouldSwitch) {
if (!taskSwitcherHelpers.taskDrawerOpened && unmodifiedYposition < taskSwitcherHelpers.openedYPosition) {
// if in a app, switch it to the new task when it is under the openedYPosition
taskList.setTaskOffsetValue(0, unmodifiedYposition < taskSwitcherHelpers.openedYPosition && taskSwitcherHelpers.notHomeScreenState);
let appAtNewIndex = taskList.getTaskAt(newIndex).window; let appAtNewIndex = taskList.getTaskAt(newIndex).window;
taskSwitcherHelpers.openApp(newIndex, appAtNewIndex, Kirigami.Units.longDuration * 4, Easing.OutExpo); taskSwitcherHelpers.openApp(newIndex, appAtNewIndex, Kirigami.Units.longDuration * 4, Easing.OutExpo);
} else {
// if already in the task switcher or above the openedYPosition, only change the focus to the new task
taskSwitcherHelpers.animateGoToTaskIndex(newIndex);
taskSwitcherHelpers.open();
}
} else { } else {
// if not switching, just open task switcher // if not switching, just open task switcher
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex); taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
@ -191,75 +361,82 @@ FocusScope {
// Logic for deciding how to handle the end of a gesture input // Logic for deciding how to handle the end of a gesture input
function onGestureInProgressChanged() { function onGestureInProgressChanged() {
taskSwitcherHelpers.fromButton = false;
if (state.gestureInProgress) { if (state.gestureInProgress) {
taskSwitcherHelpers.currentDisplayTask = state.currentTaskIndex;
return; return;
} }
if (state.status == TaskSwitcherData.TaskSwitcherState.Active) { if (taskList.count === 0) {
if (taskSwitcherHelpers.isInTaskScrubMode) { // dismiss the gesture if the task list is empty
taskSwitcherHelpers.close();
} if (taskSwitcherHelpers.isInTaskScrubMode) {
// TODO! do we want to handle upwards flick to dismiss in task scrub mode? // TODO! do we want to handle upwards flick to dismiss in task scrub mode?
// TODO do we want to show a list of thumbnails in task scrub mode? // TODO do we want to show a list of thumbnails in task scrub mode?
let unmodifiedYposition = Math.abs(state.touchYPosition)
backgroundColorOpacity = 1;
if (taskSwitcherHelpers.taskDrawerOpened || unmodifiedYposition > taskSwitcherHelpers.undoYThreshold) {
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
taskSwitcherHelpers.open();
taskSwitcherHelpers.isInTaskScrubMode = false;
} else {
taskSwitcherHelpers.openApp(state.currentTaskIndex, taskList.getTaskAt(state.currentTaskIndex).window); taskSwitcherHelpers.openApp(state.currentTaskIndex, taskList.getTaskAt(state.currentTaskIndex).window);
} else if (state.yPosition > taskSwitcherHelpers.undoYThreshold) { }
// close task switcher if it was already open but only if swipe was higher than the undo threshold } else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Undecided) {
if (taskSwitcherHelpers.taskDrawerOpened) {
// if in the task switcher, return to it
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
taskSwitcherHelpers.open();
} else if (state.wasInActiveTask) {
// if inside a app, return to it
returnToApp();
} else {
// else dismiss the gesture
taskSwitcherHelpers.close(); taskSwitcherHelpers.close();
return;
} }
} else if (state.status == TaskSwitcherData.TaskSwitcherState.Inactive) { } else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.HorizontalSwipe) {
if (state.totalSquaredVelocity > state.flickVelocityThreshold) {
// flick
// ratio between y and x velocity as threshold between vertical and horizontal flick
let xyVelocityRatio = 1.7; // with 1.7 swipes up to ~60° from horizontal are counted as horizontal
if (-state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) {
upwardsFlick();
} else if (state.yVelocity > Math.abs(state.xVelocity) * xyVelocityRatio) {
// downwards flick
returnToApp();
} else {
// sideways flick // sideways flick
backgroundColorOpacity = 1;
quickSwitch(); quickSwitch();
} } else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
} else { // open the task drawer
// no flick backgroundColorOpacity = 1;
if (state.yPosition > taskSwitcherHelpers.undoYThreshold) {
// normal task switcher open
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex); taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
taskSwitcherHelpers.open(); taskSwitcherHelpers.open();
} else { } else if (taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.Home) {
// no flick and not enough activation to go to task switcher taskSwitcherHelpers.close();
if (state.wasInActiveTask) {
returnToApp();
} else {
// do open switcher in case we were on homescreen before
taskSwitcherHelpers.animateGoToTaskIndex(state.currentTaskIndex);
state.yPosition = taskSwitcherHelpers.openedYPosition;
taskSwitcherHelpers.open();
}
}
}
} }
} }
function onVelocityChanged() { function onVelocityChanged() {
if (!taskSwitcherHelpers.hasVibrated) {
if (!state.wasInActiveTask ||
(state.wasInActiveTask &&
state.yPosition > taskSwitcherHelpers.undoYThreshold &&
state.totalSquaredVelocity < state.flickVelocityThreshold)) {
// Haptic feedback when conditions are met for the task switcher to open
haptics.buttonVibrate();
taskSwitcherHelpers.hasVibrated = true;
}
} }
}
function onXPositionChanged() { function onXPositionChanged() {
taskSwitcherHelpers.updateTaskIndex(); taskSwitcherHelpers.updateTaskIndex();
} }
} }
// kind of a hack, but this prevents the gesture from immediately activting the task switcher when it is not supposed to
Timer {
id: taskSwitchCanLaunchTimer
interval: 1; running: true; repeat: false
onTriggered: taskSwitcherHelpers.taskSwitchCanLaunch = true;
}
function setTaskDrawerState(value) {
if (taskSwitcherHelpers.gestureState != TaskSwitcherHelpers.GestureStates.TaskSwitcher && value == TaskSwitcherHelpers.GestureStates.TaskSwitcher) {
// vibrate only if switching to task drawer
if (!taskSwitcherHelpers.hasVibrated) {
// Haptic feedback when the task scrub mode engages
haptics.buttonVibrate();
taskSwitcherHelpers.hasVibrated = true;
}
}
taskSwitcherHelpers.gestureState = value;
}
// view of the desktop background // view of the desktop background
KWinComponents.DesktopBackground { KWinComponents.DesktopBackground {
id: backgroundItem id: backgroundItem
@ -275,13 +452,17 @@ FocusScope {
opacity: container.opacity opacity: container.opacity
color: { color: {
// animate background colour only if we are *not* opening from the homescreen return Qt.rgba(0, 0, 0, 0.6 * taskSwitcherHelpers.closingFactor * backgroundColorOpacity);
if (state.wasInActiveTask || !state.currentlyBeingOpened) {
return Qt.rgba(0, 0, 0, 0.6);
} else {
return Qt.rgba(0, 0, 0, 0.6 * Math.min(1, state.yPosition / state.openedYPosition));
} }
} }
// animate the background opacity based off of the state.
property real backgroundColorOpacity: 1
Behavior on backgroundColorOpacity {
id: backgroundColorOpacityAn
NumberAnimation {
duration: Kirigami.Units.longDuration
}
} }
// status bar // status bar
@ -413,7 +594,11 @@ FocusScope {
ColumnLayout { ColumnLayout {
id: placeholder id: placeholder
spacing: Kirigami.Units.gridUnit spacing: Kirigami.Units.gridUnit
opacity: (root.tasksCount === 0 && !taskSwitcherHelpers.currentlyBeingClosed) ? 0.9 : 0
opacity: {
let baseOpacity = ((root.tasksCount === 0 && !taskSwitcherHelpers.currentlyBeingClosed) ? 0.9 : 0);
return taskSwitcherHelpers.gestureState == TaskSwitcherHelpers.GestureStates.TaskSwitcher ? baseOpacity : 0;
}
Behavior on opacity { NumberAnimation { duration: 500 } } Behavior on opacity { NumberAnimation { duration: 500 } }
anchors.centerIn: parent anchors.centerIn: parent
@ -549,22 +734,22 @@ FocusScope {
x: flickable.contentX x: flickable.contentX
width: flickable.width width: flickable.width
height: flickable.height height: flickable.height
z: 1
}
PlasmaComponents.ToolButton { PlasmaComponents.ToolButton {
id: closeAllButton id: closeAllButton
property bool closeRequested: false property bool closeRequested: false
visible: root.tasksCount !== 0 && !taskSwitcherHelpers.isInTaskScrubMode visible: root.tasksCount !== 0 && !taskSwitcherHelpers.isInTaskScrubMode
enabled: !taskSwitcherHelpers.currentlyBeingClosed && !root.state.gestureInProgress
anchors { anchors {
bottom: parent.bottom bottom: parent.bottom
bottomMargin: (taskList.taskY + taskList.trackFingerYOffset) / 2 bottomMargin: (taskList.taskYBase) * 0.75
horizontalCenter: parent.horizontalCenter horizontalCenter: taskList.horizontalCenter
} }
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary opacity: (taskSwitcherHelpers.currentlyBeingClosed || root.state.gestureInProgress || !taskSwitcherHelpers.taskDrawerOpened) ? 0.0 : 1.0
Kirigami.Theme.inherit: false
opacity: (taskSwitcherHelpers.currentlyBeingClosed) ? 0.0 : 1.0
Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } } Behavior on opacity { NumberAnimation { duration: Kirigami.Units.shortDuration } }
icon.name: "edit-clear-history" icon.name: "edit-clear-history"
@ -582,6 +767,5 @@ FocusScope {
} }
} }
} }
}
} }

View file

@ -4,6 +4,7 @@
import QtQuick 2.15 import QtQuick 2.15
import org.kde.kirigami 2.20 as Kirigami import org.kde.kirigami 2.20 as Kirigami
import org.kde.private.mobileshell.taskswitcher 1.0 as TaskSwitcherData
import org.kde.kwin 3.0 as KWinComponents import org.kde.kwin 3.0 as KWinComponents
@ -14,11 +15,27 @@ QtObject {
id: root id: root
// TaskSwitcher item component // TaskSwitcher item component
// We assume that the taskSwitcher the size of the entire screen. // We assume that the taskSwitcher is the size of the entire screen.
required property var taskSwitcher required property var taskSwitcher
property var state: taskSwitcher.state property var state: taskSwitcher.state
required property var stateClass required property var stateClass
// task switcher peek and pop setting for when it is toggled from the home screen
readonly property real peekOffsetValue: 1.85
readonly property real homeOffsetValue: 2.6
readonly property real taskOffsetValue: 1.5
// the index of the last task to be closed
// this will get reset to -1 in TaskSwitcher.qml when the TasksCount changes
property int lastClosedTask: -1
// this will only gets set to the currentTaskIndex when the gesture starts
// this helps remove visual glitches as the gesture animations play out and the currentTaskIndex changes
property int currentDisplayTask: state.currentTaskIndex
// how much the the task will resist shrinking
// value of 1 will match the y position resistance one to one
readonly property real scaleResistance: 0.5
// direction of the movement // direction of the movement
readonly property bool gestureMovingRight: state.xVelocity > 0 readonly property bool gestureMovingRight: state.xVelocity > 0
@ -33,10 +50,37 @@ QtObject {
// yPosition threshold below which opening the task switcher should be undone and returned to the previously active task // yPosition threshold below which opening the task switcher should be undone and returned to the previously active task
readonly property real undoYThreshold: openedYPosition / 2 readonly property real undoYThreshold: openedYPosition / 2
// the height threshold where if the yPosition is above this value the task switch will return home
readonly property real heightThreshold: windowHeight * 0.55
// whether the switcher is opened or not
readonly property bool taskDrawerOpened: state.status == TaskSwitcherData.TaskSwitcherState.Active
// This is true when the task drawer is already opened or if within an app
readonly property bool notHomeScreenState: state.wasInActiveTask || taskDrawerOpened
// set to true if the taskSwitcher is opened by the navbar button
property bool fromButton: false
// gets set to true after 1 milliseconds
property bool taskSwitchCanLaunch: false
// whether the switcher has already triggered haptic feedback or not // whether the switcher has already triggered haptic feedback or not
// we don't want to continuously send haptics, just once is enough // we don't want to continuously send haptics, just once is enough
property bool hasVibrated: false property bool hasVibrated: false
// The current gesture state to decide what will happpen when it is completed
enum GestureStates {
Undecided,
HorizontalSwipe,
TaskSwitcher,
Home
}
property int gestureState: TaskSwitcherHelpers.GestureStates.Undecided
// if the touch has reached the height threshold
property bool reachedHeightThreshold: false
// made as variables to keep x anim in task list and task scrub icon list in sync // made as variables to keep x anim in task list and task scrub icon list in sync
property int xAnimDuration: Kirigami.Units.longDuration * 2 property int xAnimDuration: Kirigami.Units.longDuration * 2
property int xAnimEasingType: Easing.OutExpo property int xAnimEasingType: Easing.OutExpo
@ -60,6 +104,24 @@ QtObject {
// height of the task preview header // height of the task preview header
readonly property real taskHeaderHeight: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2 readonly property real taskHeaderHeight: Kirigami.Units.gridUnit * 2 + Kirigami.Units.smallSpacing * 2
// finger position y with resistance
readonly property real trackFingerYOffset: {
if (taskSwitcherHelpers.isScaleClamped) {
let directTrackingOffset = openedYPosition * 0.2
if (root.state.yPosition < openedYPosition + directTrackingOffset) {
// Allow the task list to move further up than the fully opened position
return root.state.yPosition - openedYPosition;
} else {
// but make it more reluctant the further up it goes
let overDragProgress = (root.state.yPosition - directTrackingOffset - openedYPosition) / openedYPosition;
// Base formula is 1-2.3^(-progress) which asymptotically approaches 1
return (1 - Math.pow(2.3, -overDragProgress)) * openedYPosition + directTrackingOffset;
}
} else {
return 0;
}
}
// the scaling factor of the window preview compared to the actual window // the scaling factor of the window preview compared to the actual window
// we need to ensure that window previews always fit on screen // we need to ensure that window previews always fit on screen
readonly property real scalingFactor: { readonly property real scalingFactor: {
@ -75,27 +137,26 @@ QtObject {
} }
} }
// scaling factor during closing of the switcher // the closing factor during the closing of the switcher
property real closingFactor: 1
// scaling factor during the closing of the switcher
property real closingScalingFactor: 1 property real closingScalingFactor: 1
// scale of the task list (based on the progress of the swipe up gesture) // scale of the task list (based on the progress of the swipe up gesture)
readonly property real currentScale: { readonly property real currentScale: {
let maxScale = 1 / scalingFactor; let maxScale = 1 / scalingFactor;
let subtract = (maxScale - 1) * Math.min(root.state.yPosition / openedYPosition, 1); let subtract = (maxScale - 1) * ((Math.min(root.state.yPosition, openedYPosition) + trackFingerYOffset * scaleResistance) / openedYPosition)
let finalScale = Math.min(maxScale, maxScale - subtract); let finalScale = Math.min(maxScale, maxScale - subtract);
// if closing scaling factor is below 1 we want it to override the other scale // if closing scaling factor is below 1 we want it to override the other scale
// to allow for a smoother closing animation // to allow for a smoother closing animation
if (closingScalingFactor < 1) { if (closingScalingFactor < 1 && (root.state.wasInActiveTask || root.taskDrawerOpened)) {
return closingScalingFactor; return closingScalingFactor;
} }
// animate scale only if we are *not* opening from the homescreen
if (root.state.wasInActiveTask || !root.state.gestureInProgress) {
return finalScale; return finalScale;
} }
return 1;
}
readonly property bool isScaleClamped: root.state.yPosition > openedYPosition readonly property bool isScaleClamped: root.state.yPosition > openedYPosition
readonly property real taskScrubDistance: windowWidth / (2 * 6) // formula says how many tasks can be scrubbed through in half of the window width readonly property real taskScrubDistance: windowWidth / (2 * 6) // formula says how many tasks can be scrubbed through in half of the window width
@ -106,10 +167,10 @@ QtObject {
// cancel all animated moving, as another flick source is taking over // cancel all animated moving, as another flick source is taking over
signal cancelAnimations() signal cancelAnimations()
onCancelAnimations: { onCancelAnimations: {
openAnim.stop();
openAppAnim.stop(); openAppAnim.stop();
closeAnim.stop(); closeAnim.stop();
closeScaleAnim.stop(); closeScaleAnim.stop();
closeFactorAnim.stop();
xAnim.stop(); xAnim.stop();
} }
@ -130,22 +191,35 @@ QtObject {
// TODO either use updateTaskIndex to always have the "newest current task index" in the state var or use "getNearestTaskIndex", not both it's redundant // TODO either use updateTaskIndex to always have the "newest current task index" in the state var or use "getNearestTaskIndex", not both it's redundant
function updateTaskIndex() { function updateTaskIndex() {
// only set if not gesture currently in progress to prevent glitching
if (!(state.gestureInProgress || root.closeAnim.running || root.openAppAnim.running) || root.isInTaskScrubMode) {
root.state.currentTaskIndex = getTaskIndexFromXPosition(); root.state.currentTaskIndex = getTaskIndexFromXPosition();
} }
}
function open() { function open() {
root.gestureState = TaskSwitcherHelpers.GestureStates.TaskSwitcher;
openAnim.restart(); openAnim.restart();
// update the task offset position
taskList.setTaskOffsetValue(0, false, Easing.OutQuart);
} }
function close() { function close() {
// update the task offset position
taskList.setTaskOffsetValue(homeOffsetValue + 0.25, false, Easing.Linear);
root.gestureState = TaskSwitcherHelpers.GestureStates.Undecided;
cancelAnimations(); cancelAnimations();
closingScalingFactor = currentScale; closingScalingFactor = currentScale;
closeAnim.restart(); closeAnim.restart();
closeScaleAnim.restart(); closeScaleAnim.restart();
closeFactorAnim.restart();
} }
function openApp(index, window, duration = Kirigami.Units.shortDuration, horizontalEasing = Easing.OutBack) { function openApp(index, window, duration = Kirigami.Units.shortDuration, horizontalEasing = Easing.OutBack) {
// cancel any opening animations ongoing // cancel any opening animations ongoing
openAnim.stop();
cancelAnimations(); cancelAnimations();
animateGoToTaskIndex(index, duration); animateGoToTaskIndex(index, duration);
@ -167,7 +241,7 @@ QtObject {
function animateGoToTaskIndex(index, duration = Kirigami.Units.longDuration * 2, easing = Easing.OutExpo) { function animateGoToTaskIndex(index, duration = Kirigami.Units.longDuration * 2, easing = Easing.OutExpo) {
xAnimDuration = duration; xAnimDuration = duration;
xAnimEasingType = easing; xAnimEasingType = easing;
xAnim.to = xPositionFromTaskIndex(index); xAnim.to = xPositionFromTaskIndex(index) - (gestureState == TaskSwitcherHelpers.GestureStates.HorizontalSwipe && !state.gestureInProgress && notHomeScreenState ? taskSpacing / 2 : 0);
xAnim.restart(); xAnim.restart();
} }
@ -232,12 +306,14 @@ QtObject {
property: "yPosition" property: "yPosition"
to: openedYPosition to: openedYPosition
duration: 250 duration: 250
easing.type: Easing.OutExpo easing.type: Easing.OutQuart
onFinished: { onFinished: {
if (!isInTaskScrubMode) {
root.state.status = stateClass.Active; root.state.status = stateClass.Active;
} }
} }
}
// TODO: This animation should ideally be replaced by some // TODO: This animation should ideally be replaced by some
// speed tracking to track finger movement better. Until then // speed tracking to track finger movement better. Until then
@ -267,6 +343,14 @@ QtObject {
} }
} }
property var closeFactorAnim: NumberAnimation {
target: root
property: "closingFactor"
to: 0
duration: Kirigami.Units.longDuration
easing.type: Easing.InQuad
}
property var openAppAnim: NumberAnimation { property var openAppAnim: NumberAnimation {
target: root.state target: root.state
property: "yPosition" property: "yPosition"