| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.wm.shell.common.split; |
| |
| import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; |
| import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; |
| |
| import android.content.Context; |
| import android.util.AttributeSet; |
| import android.view.GestureDetector; |
| import android.view.MotionEvent; |
| import android.view.SurfaceControlViewHost; |
| import android.view.VelocityTracker; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.WindowManager; |
| import android.widget.FrameLayout; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.policy.DividerSnapAlgorithm; |
| import com.android.wm.shell.R; |
| import com.android.wm.shell.animation.Interpolators; |
| |
| /** |
| * Divider for multi window splits. |
| */ |
| public class DividerView extends FrameLayout implements View.OnTouchListener { |
| public static final long TOUCH_ANIMATION_DURATION = 150; |
| public static final long TOUCH_RELEASE_ANIMATION_DURATION = 200; |
| |
| private final int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); |
| |
| private SplitLayout mSplitLayout; |
| private SurfaceControlViewHost mViewHost; |
| private DividerHandleView mHandle; |
| private View mBackground; |
| private int mTouchElevation; |
| |
| private VelocityTracker mVelocityTracker; |
| private boolean mMoving; |
| private int mStartPos; |
| private GestureDetector mDoubleTapDetector; |
| private boolean mInteractive; |
| |
| public DividerView(@NonNull Context context) { |
| super(context); |
| } |
| |
| public DividerView(@NonNull Context context, |
| @Nullable AttributeSet attrs) { |
| super(context, attrs); |
| } |
| |
| public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| } |
| |
| public DividerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, |
| int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| } |
| |
| /** Sets up essential dependencies of the divider bar. */ |
| public void setup( |
| SplitLayout layout, |
| SurfaceControlViewHost viewHost) { |
| mSplitLayout = layout; |
| mViewHost = viewHost; |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| super.onFinishInflate(); |
| mHandle = findViewById(R.id.docked_divider_handle); |
| mBackground = findViewById(R.id.docked_divider_background); |
| mTouchElevation = getResources().getDimensionPixelSize( |
| R.dimen.docked_stack_divider_lift_elevation); |
| mDoubleTapDetector = new GestureDetector(getContext(), new DoubleTapListener()); |
| mInteractive = true; |
| setOnTouchListener(this); |
| } |
| |
| @Override |
| public boolean onTouch(View v, MotionEvent event) { |
| if (mSplitLayout == null || !mInteractive) { |
| return false; |
| } |
| |
| if (mDoubleTapDetector.onTouchEvent(event)) { |
| return true; |
| } |
| |
| final int action = event.getAction() & MotionEvent.ACTION_MASK; |
| final boolean isLandscape = isLandscape(); |
| // Using raw xy to prevent lost track of motion events while moving divider bar. |
| final int touchPos = isLandscape ? (int) event.getRawX() : (int) event.getRawY(); |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mVelocityTracker = VelocityTracker.obtain(); |
| mVelocityTracker.addMovement(event); |
| setTouching(); |
| mStartPos = touchPos; |
| mMoving = false; |
| break; |
| case MotionEvent.ACTION_MOVE: |
| mVelocityTracker.addMovement(event); |
| if (!mMoving && Math.abs(touchPos - mStartPos) > mTouchSlop) { |
| mStartPos = touchPos; |
| mMoving = true; |
| } |
| if (mMoving) { |
| final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; |
| mSplitLayout.updateDivideBounds(position); |
| } |
| break; |
| case MotionEvent.ACTION_UP: |
| case MotionEvent.ACTION_CANCEL: |
| releaseTouching(); |
| if (!mMoving) break; |
| |
| mVelocityTracker.addMovement(event); |
| mVelocityTracker.computeCurrentVelocity(1000 /* units */); |
| final float velocity = isLandscape |
| ? mVelocityTracker.getXVelocity() |
| : mVelocityTracker.getYVelocity(); |
| final int position = mSplitLayout.getDividePosition() + touchPos - mStartPos; |
| final DividerSnapAlgorithm.SnapTarget snapTarget = |
| mSplitLayout.findSnapTarget(position, velocity, false /* hardDismiss */); |
| mSplitLayout.snapToTarget(position, snapTarget); |
| mMoving = false; |
| break; |
| } |
| |
| return true; |
| } |
| |
| private void setTouching() { |
| setSlippery(false); |
| mHandle.setTouching(true, true); |
| if (isLandscape()) { |
| mBackground.animate().scaleX(1.4f); |
| } else { |
| mBackground.animate().scaleY(1.4f); |
| } |
| mBackground.animate() |
| .setInterpolator(Interpolators.TOUCH_RESPONSE) |
| .setDuration(TOUCH_ANIMATION_DURATION) |
| .translationZ(mTouchElevation) |
| .start(); |
| // Lift handle as well so it doesn't get behind the background, even though it doesn't |
| // cast shadow. |
| mHandle.animate() |
| .setInterpolator(Interpolators.TOUCH_RESPONSE) |
| .setDuration(TOUCH_ANIMATION_DURATION) |
| .translationZ(mTouchElevation) |
| .start(); |
| } |
| |
| private void releaseTouching() { |
| setSlippery(true); |
| mHandle.setTouching(false, true); |
| mBackground.animate() |
| .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) |
| .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) |
| .translationZ(0) |
| .scaleX(1f) |
| .scaleY(1f) |
| .start(); |
| mHandle.animate() |
| .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) |
| .setDuration(TOUCH_RELEASE_ANIMATION_DURATION) |
| .translationZ(0) |
| .start(); |
| } |
| |
| private void setSlippery(boolean slippery) { |
| if (mViewHost == null) { |
| return; |
| } |
| |
| final WindowManager.LayoutParams lp = (WindowManager.LayoutParams) getLayoutParams(); |
| final boolean isSlippery = (lp.flags & FLAG_SLIPPERY) != 0; |
| if (isSlippery == slippery) { |
| return; |
| } |
| |
| if (slippery) { |
| lp.flags |= FLAG_SLIPPERY; |
| } else { |
| lp.flags &= ~FLAG_SLIPPERY; |
| } |
| mViewHost.relayout(lp); |
| } |
| |
| void setInteractive(boolean interactive) { |
| if (interactive == mInteractive) return; |
| mInteractive = interactive; |
| releaseTouching(); |
| mHandle.setVisibility(mInteractive ? View.VISIBLE : View.INVISIBLE); |
| } |
| |
| private boolean isLandscape() { |
| return getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE; |
| } |
| |
| private class DoubleTapListener extends GestureDetector.SimpleOnGestureListener { |
| @Override |
| public boolean onDoubleTap(MotionEvent e) { |
| if (mSplitLayout != null) { |
| mSplitLayout.onDoubleTappedDivider(); |
| } |
| return true; |
| } |
| } |
| } |