在android中处于放大模式时,滚动到二维滚动视图中不工作的边
这很难解释,但我正在努力 在我的例子中,有一个线性布局(使用滚动到边缘功能放大)和多个recyclerView(用于拖放功能)。下面是缩放时滚动到边的问题。我尝试了一种解决方案,比如使用2个滚动视图1个水平视图1个垂直视图,但在这种情况下,它一次只能将视图移动一个方向 我从林克那里得到了这门课的参考资料 这里我附上我的二维滚动视图类在android中处于放大模式时,滚动到二维滚动视图中不工作的边,android,android-layout,drag-and-drop,android-scrollview,pinchzoom,Android,Android Layout,Drag And Drop,Android Scrollview,Pinchzoom,这很难解释,但我正在努力 在我的例子中,有一个线性布局(使用滚动到边缘功能放大)和多个recyclerView(用于拖放功能)。下面是缩放时滚动到边的问题。我尝试了一种解决方案,比如使用2个滚动视图1个水平视图1个垂直视图,但在这种情况下,它一次只能将视图移动一个方向 我从林克那里得到了这门课的参考资料 这里我附上我的二维滚动视图类 package com.app.xyz import android.annotation.SuppressLint import android.content
package com.app.xyz
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.*
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
import android.view.ViewGroup
import android.view.animation.AnimationUtils
import android.view.animation.ScaleAnimation
import android.widget.LinearLayout
import android.widget.Scroller
import timber.log.Timber
import java.lang.String
import java.util.*
class TwoDScrollView : LinearLayout {
val ANIMATED_SCROLL_GAP = 250
val TWO_DSCROLL_VIEW_CAN_HOST_ONLY_ONE_DIRECT_CHILD =
"TwoDScrollView can host only one direct child"
private var mLastScroll: Long = 0
private var mScroller: Scroller? = null
/**
* Position of the last motion event.
*/
private var mLastMotionY = 0f
private var mLastMotionX = 0f
/**
* True if the user is currently dragging this TwoDScrollView around. This is
* not the same as 'is being flinged', which can be checked by
* mScroller.isFinished() (flinging begins when the user lifts his finger).
*/
private var mIsBeingDragged = false
/**
* Determines speed during touch scrolling
*/
private var mVelocityTracker: VelocityTracker? = null
/**
* Whether arrow scrolling is animated.
*/
private var mTouchSlop = 0
private var mMinimumVelocity = 0
private var mScale = 1f
private var mScaleDetector: ScaleGestureDetector? = null
private var gestureDetector: GestureDetector? = null
private val mEnableScaling =
true // scaling is buggy when you click on child views
constructor(context: Context?) : super(context!!) {
initTwoDScrollView()
}
constructor(context: Context?, attrs: AttributeSet?) : super(
context!!,
attrs
) {
initTwoDScrollView()
}
override fun getTopFadingEdgeStrength(): Float {
if (childCount == 0) {
return 0.0f
}
val length = verticalFadingEdgeLength
return if (scrollY < length) {
scrollY / length.toFloat()
} else 1.0f
}
override fun getBottomFadingEdgeStrength(): Float {
if (childCount == 0) {
return 0.0f
}
val length = verticalFadingEdgeLength
val bottomEdge = height - paddingBottom
val span = getChildAt(0).bottom - scrollY - bottomEdge
return if (span < length) {
span / length.toFloat()
} else 1.0f
}
override fun getLeftFadingEdgeStrength(): Float {
if (childCount == 0) {
return 0.0f
}
val length = horizontalFadingEdgeLength
return if (scrollX < length) {
scrollX / length.toFloat()
} else 1.0f
}
override fun getRightFadingEdgeStrength(): Float {
if (childCount == 0) {
return 0.0f
}
val length = horizontalFadingEdgeLength
val rightEdge = width - paddingRight
val span = getChildAt(0).right - scrollX - rightEdge
return if (span < length) {
span / length.toFloat()
} else 1.0f
}
private fun initTwoDScrollView() {
mScroller = Scroller(context)
isFocusable = true
descendantFocusability = ViewGroup.FOCUS_AFTER_DESCENDANTS
setWillNotDraw(false)
val configuration = ViewConfiguration.get(context)
mTouchSlop = configuration.scaledTouchSlop
mMinimumVelocity = configuration.scaledMinimumFlingVelocity
if (mEnableScaling) {
gestureDetector = GestureDetector(context, GestureListener())
}
}
override fun addView(child: View) {
check(childCount <= 0) { TWO_DSCROLL_VIEW_CAN_HOST_ONLY_ONE_DIRECT_CHILD }
super.addView(child)
if (mEnableScaling) {
createScaleGestureDetector(child)
}
}
override fun addView(child: View, index: Int) {
check(childCount <= 0) { TWO_DSCROLL_VIEW_CAN_HOST_ONLY_ONE_DIRECT_CHILD }
super.addView(child, index)
if (mEnableScaling) {
createScaleGestureDetector(child)
}
}
override fun addView(child: View, params: ViewGroup.LayoutParams?) {
check(childCount <= 0) { TWO_DSCROLL_VIEW_CAN_HOST_ONLY_ONE_DIRECT_CHILD }
super.addView(child, params)
if (mEnableScaling) {
createScaleGestureDetector(child)
}
}
override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) {
check(childCount <= 0) { TWO_DSCROLL_VIEW_CAN_HOST_ONLY_ONE_DIRECT_CHILD }
super.addView(child, index, params)
if (mEnableScaling) {
createScaleGestureDetector(child)
}
}
private fun createScaleGestureDetector(childLayout: View) {
mScaleDetector =
ScaleGestureDetector(context, object : SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
val scale = 1 - detector.scaleFactor
val prevScale = mScale
mScale += scale
if (mScale < 0.5f) // Minimum scale condition:
mScale = 0.5f
if (mScale > 2.5f) // Maximum scale condition:
mScale = 2.5f
val scaleAnimation = ScaleAnimation(
1f / prevScale, 1f / mScale,
1f / prevScale, 1f / mScale,
detector.focusX, detector.focusY
)
scaleAnimation.duration = 0
scaleAnimation.fillAfter = true
childLayout.startAnimation(scaleAnimation)
return true
}
})
}
/**
* @return Returns true this TwoDScrollView can be scrolled
*/
private fun canScroll(): Boolean {
val child = getChildAt(0)
if (child != null) {
val childHeight = child.height
val childWidth = child.width
return height < childHeight + paddingTop + paddingBottom ||
width < childWidth + paddingLeft + paddingRight
}
return false
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onMotionEvent will be called and we do the actual
* scrolling there.
*/
/*
* Shortcut the most recurring case: the user is in the dragging
* state and he is moving his finger. We want to intercept this
* motion.
*/
val action = ev.action
if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
return true
}
if (!canScroll()) {
mIsBeingDragged = false
return false
}
val y = ev.y
val x = ev.x
when (action) {
MotionEvent.ACTION_MOVE -> {
/*
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
* whether the user has moved far enough from his original down touch.
*/
/*
* Locally do absolute value. mLastMotionY is set to the y value
* of the down event.
*/
val yDiff = Math.abs(y - mLastMotionY).toInt()
val xDiff = Math.abs(x - mLastMotionX).toInt()
if (yDiff > mTouchSlop || xDiff > mTouchSlop) {
mIsBeingDragged = true
}
}
MotionEvent.ACTION_DOWN -> {
/* Remember location of down touch */mLastMotionY = y
mLastMotionX = x
/*
* If being flinged and user touches the screen, initiate drag...
* otherwise don't. mScroller.isFinished should be false when
* being flinged.
*/mIsBeingDragged = false
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> /* Release the drag */mIsBeingDragged =
false
}
/*
* The only time we want to intercept motion events is if we are in the
* drag mode.
*/return mIsBeingDragged
}
override fun onTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN && ev.edgeFlags != 0) {
// Don't handle edge touches immediately -- they may actually belong to one of our
// descendants.
return false
}
if (!canScroll()) {
return false
}
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain()
}
mVelocityTracker!!.addMovement(ev)
val action = ev.action
val y = ev.y
val x = ev.x
when (action) {
MotionEvent.ACTION_DOWN -> {
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/if (!mScroller!!.isFinished) {
mScroller!!.abortAnimation()
}
// Remember where the motion event started
mLastMotionY = y
mLastMotionX = x
}
MotionEvent.ACTION_MOVE -> {
// Scroll to follow the motion event
var deltaX = (mLastMotionX - x).toInt()
var deltaY = (mLastMotionY - y).toInt()
mLastMotionX = x
mLastMotionY = y
if (deltaX < 0) {
if (scrollX < 0) {
deltaX = 0
}
} else if (deltaX > 0) {
val rightEdge = width - paddingRight
val availableToScroll = getChildAt(0).right - scrollX - rightEdge
deltaX = if (availableToScroll > 0) {
Math.min(availableToScroll, deltaX)
} else {
0
}
}
if (deltaY < 0) {
if (scrollY < 0) {
deltaY = 0
}
} else if (deltaY > 0) {
val bottomEdge = height - paddingBottom
val availableToScroll =
getChildAt(0).bottom - scrollY - bottomEdge
deltaY = if (availableToScroll > 0) {
Math.min(availableToScroll, deltaY)
} else {
0
}
}
if (deltaY != 0 || deltaX != 0) scrollBy(deltaX, deltaY)
}
MotionEvent.ACTION_UP -> {
val velocityTracker = mVelocityTracker
velocityTracker!!.computeCurrentVelocity(1000)
val initialXVelocity = velocityTracker.xVelocity.toInt()
val initialYVelocity = velocityTracker.yVelocity.toInt()
if (Math.abs(initialXVelocity) + Math.abs(initialYVelocity) > mMinimumVelocity && childCount > 0) {
fling(-initialXVelocity, -initialYVelocity)
}
if (mVelocityTracker != null) {
mVelocityTracker!!.recycle()
mVelocityTracker = null
}
}
}
return true
}
@SuppressLint("StringFormatInTimber")
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
return if (mEnableScaling) {
super.dispatchTouchEvent(event)
mScaleDetector?.onTouchEvent(event)
// scale event positions according to scale before passing them to children
Timber.d(
TwoDScrollView::class.java.simpleName,
String.format(
Locale.getDefault(), "Position (%.2f,%.2f) ScrollOffset (%d,%d) Scale %.2f",
event.x, event.y, scrollX, scrollY, mScale
)
)
event.setLocation(
(scrollX + event.x) * mScale,
(scrollY + event.y) * mScale
)
gestureDetector!!.onTouchEvent(event)
} else {
super.dispatchTouchEvent(event)
}
}
/**
*
* Handles scrolling in response to a "home/end" shortcut press.
*
* @param directionVert the scroll direction: [android.view.View.FOCUS_UP]
* to go the top of the view or
* [android.view.View.FOCUS_DOWN] to go the bottom
* @param directionHorz the scroll direction: [android.view.View.FOCUS_RIGHT]
* to go the right of the view or
* [android.view.View.FOCUS_LEFT] to go the left
* @return true if the key event is consumed by this method, false otherwise
*/
fun fullScroll(directionVert: Int, directionHorz: Int): Boolean {
var scrollAmountY = 0
var scrollAmountX = 0
when (directionVert) {
View.FOCUS_UP -> scrollAmountY = -scrollY
View.FOCUS_DOWN -> {
val count = childCount
if (count > 0) {
val view = getChildAt(count - 1)
scrollAmountY = view.bottom - height - scrollY
}
}
}
when (directionHorz) {
View.FOCUS_LEFT -> scrollAmountX = -scrollX
View.FOCUS_RIGHT -> {
val count = childCount
if (count > 0) {
val view = getChildAt(count - 1)
scrollAmountX = view.right - width - scrollX
}
}
}
val handled = scrollAmountX != 0 || scrollAmountY != 0
if (handled) doScroll(scrollAmountX, scrollAmountY)
return handled
}
/**
* Smooth scroll by a Y delta
*
* @param deltaX the number of pixels to scroll by on the X axis
* @param deltaY the number of pixels to scroll by on the Y axis
*/
private fun doScroll(deltaX: Int, deltaY: Int) {
if (deltaX != 0 || deltaY != 0) {
smoothScrollBy(deltaX, deltaY)
}
}
/**
* Like [View.scrollBy], but scroll smoothly instead of immediately.
*
* @param dx the number of pixels to scroll by on the X axis
* @param dy the number of pixels to scroll by on the Y axis
*/
fun smoothScrollBy(dx: Int, dy: Int) {
val duration =
AnimationUtils.currentAnimationTimeMillis() - mLastScroll
if (duration > ANIMATED_SCROLL_GAP) {
mScroller!!.startScroll(scrollX, scrollY, dx, dy)
awakenScrollBars(mScroller!!.duration)
invalidate()
} else {
if (!mScroller!!.isFinished) {
mScroller!!.abortAnimation()
}
scrollBy(dx, dy)
}
mLastScroll = AnimationUtils.currentAnimationTimeMillis()
}
/**
* Like [.scrollTo], but scroll smoothly instead of immediately.
*
* @param x the position where to scroll on the X axis
* @param y the position where to scroll on the Y axis
*/
fun smoothScrollTo(x: Int, y: Int) {
smoothScrollBy(x - scrollX, y - scrollY)
}
/**
*
* The scroll range of a scroll view is the overall height of all of its
* children.
*/
override fun computeVerticalScrollRange(): Int {
val count = childCount
return if (count == 0) height else getChildAt(0).bottom
}
override fun computeHorizontalScrollRange(): Int {
val count = childCount
return if (count == 0) width else getChildAt(0).right
}
override fun measureChild(
child: View,
parentWidthMeasureSpec: Int,
parentHeightMeasureSpec: Int
) {
val lp = child.layoutParams
val childWidthMeasureSpec: Int
val childHeightMeasureSpec: Int
childWidthMeasureSpec = ViewGroup.getChildMeasureSpec(
parentWidthMeasureSpec,
paddingLeft + paddingRight,
lp.width
)
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
override fun measureChildWithMargins(
child: View,
parentWidthMeasureSpec: Int,
widthUsed: Int,
parentHeightMeasureSpec: Int,
heightUsed: Int
) {
val lp = child.layoutParams as MarginLayoutParams
val childWidthMeasureSpec =
MeasureSpec.makeMeasureSpec(lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED)
val childHeightMeasureSpec =
MeasureSpec.makeMeasureSpec(lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED)
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
override fun computeScroll() {
if (mScroller!!.computeScrollOffset()) {
// This is called at drawing time by ViewGroup. We don't want to
// re-show the scrollbars at this point, which scrollTo will do,
// so we replicate most of scrollTo here.
//
// It's a little odd to call onScrollChanged from inside the drawing.
//
// It is, except when you remember that computeScroll() is used to
// animate scrolling. So unless we want to defer the onScrollChanged()
// until the end of the animated scrolling, we don't really have a
// choice here.
//
// I agree. The alternative, which I think would be worse, is to post
// something and tell the subclasses later. This is bad because there
// will be a window where mScrollX/Y is different from what the app
// thinks it is.
//
val oldX = scrollX
val oldY = scrollY
val x = mScroller!!.currX
val y = mScroller!!.currY
if (childCount > 0) {
val child = getChildAt(0)
scrollTo(
clamp(x, width - paddingRight - paddingLeft, child.width),
clamp(y, height - paddingBottom - paddingTop, child.height)
)
} else {
scrollTo(x, y)
}
if (oldX != scrollX || oldY != scrollY) {
onScrollChanged(scrollX, scrollY, oldX, oldY)
}
// Keep on drawing until the animation has finished.
postInvalidate()
}
}
override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int
) {
super.onLayout(changed, l, t, r, b)
// Calling this with the present values causes it to re-clam them
scrollTo(scrollX, scrollY)
}
/**
* Fling the scroll view
*
* @param velocityY The initial velocity in the Y direction. Positive
* numbers mean that the finger/curor is moving down the screen,
* which means we want to scroll towards the top.
*/
fun fling(velocityX: Int, velocityY: Int) {
if (childCount > 0) {
val height = height - paddingBottom - paddingTop
val bottom = getChildAt(0).height
val width = width - paddingRight - paddingLeft
val right = getChildAt(0).width
mScroller!!.fling(
scrollX,
scrollY,
velocityX,
velocityY,
0,
right - width,
0,
bottom - height
)
awakenScrollBars(mScroller!!.duration)
invalidate()
}
}
/**
* {@inheritDoc}
*
*
* This version also clamps the scrolling to the bounds of our child.
*/
override fun scrollTo(x: Int, y: Int) {
// we rely on the fact the View.scrollBy calls scrollTo.
var x = x
var y = y
if (childCount > 0) {
val child = getChildAt(0)
x = clamp(x, width - paddingRight - paddingLeft, child.width)
y = clamp(y, height - paddingBottom - paddingTop, child.height)
if (x != scrollX || y != scrollY) {
super.scrollTo(x, y)
}
}
}
private fun clamp(n: Int, my: Int, child: Int): Int {
if (my >= child || n < 0) {
/* my >= child is this case:
* |--------------- me ---------------|
* |------ child ------|
* or
* |--------------- me ---------------|
* |------ child ------|
* or
* |--------------- me ---------------|
* |------ child ------|
*
* n < 0 is this case:
* |------ me ------|
* |-------- child --------|
* |-- mScrollX --|
*/
return 0
}
return if (my + n > child) {
/* this case:
* |------ me ------|
* |------ child ------|
* |-- mScrollX --|
*/
child - my
} else n
}
private class GestureListener : SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean {
return true
}
// event when double tap occurs
override fun onDoubleTap(e: MotionEvent): Boolean {
// double tap fired.
return true
}
}
}
package com.app.xyz
导入android.annotation.SuppressLint
导入android.content.Context
导入android.util.AttributeSet
导入android.view*
导入android.view.GestureDetector.SimpleOnGestureListener
导入android.view.scalegestruedetector.SimpleOnScalegestrueListener
导入android.view.ViewGroup
导入android.view.animation.AnimationUtils
导入android.view.animation.ScaleAnimation
导入android.widget.LinearLayout
导入android.widget.Scroller
进口木材
导入java.lang.String
导入java.util*
第二类CrollView:线性布局{
val动画_滚动_间隙=250
val TWO\u DSCROLL\u VIEW\u CAN\u HOST\u ONLY\u ONE\u DIRECT\u CHILD=
“TwoDScrollView只能承载一个直接子级”
私有变量MLASTCROLL:Long=0
专用滚动条:滚动条?=null
/**
*最后一个运动事件的位置。
*/
专用变量mLastMotionY=0f
专用变量mLastMotionX=0f
/**
*如果用户当前正在拖动此TwoDScrollView,则为True。这是
*与“正在被扔”不同,可以通过
*mScroller.isFinished()。
*/
private var misbeingdraugd=false
/**
*确定触摸滚动期间的速度
*/
私有变量mVelocityTracker:VelocityTracker?=null
/**
*箭头滚动是否已设置动画。
*/
私有变量mTouchSlop=0
私有变量mMinimumVelocity=0
专用变量mScale=1f
专用变量mScaleDetector:ScaleGetStureDetector?=null
私有变量gestureDetector:gestureDetector?=null
私有val mEnableScaling=
true//单击子视图时缩放有问题
构造函数(上下文:上下文?):超级(上下文!!){
initTwoDScrollView()
}
构造函数(上下文:context?,属性集?):super(
上下文
属性
) {
initTwoDScrollView()
}
覆盖乐趣getTopFadingEdgeStrength():浮点{
if(childCount==0){
返回0.0f
}
val长度=垂直渐变边长度
返回if(滚动<长度){
scrollY/length.toFloat()
}其他1.0f
}
覆盖有趣的getBottomFadingEdgeStrength():Float{
if(childCount==0){
返回0.0f
}
val长度=垂直渐变边长度
val bottomEdge=高度-填充底部
val span=getChildAt(0).底部-滚动-底部边缘
返回if(跨度<长度){
span/length.toFloat()
}其他1.0f
}
覆盖有趣的getLeftFadingEdgeStrength():Float{
if(childCount==0){
返回0.0f
}
val长度=水平渐变边长度
返回if(scrollX{
/*记住下触的位置*/mLastMotionY=y
mLastMotionX=x
/*
*如果被投掷,用户触摸屏幕,启动拖动。。。
*否则,当
*被抛弃。
*/错误被拖动=错误
}
MotionEvent.ACTION\u取消,MotionEvent.ACTION\u向上->/*释放拖动*/misbeingdraugd=
假的
}
/*
*我们想要截取运动事件的唯一时间是在
*拖动模式。
*/返回错误被拖动
}
覆盖事件(ev:MotionEvent):布尔值{
如果(ev.action==MotionEvent.action\u DOWN&&ev.edgeFlags!=0){
//不要立即处理边缘接触——它们实际上可能属于我们的一个团队
//后代。
返回错误
}
如果(!canScroll()){
返回错误
}
if(mVelocityTracker==null){
mVelocityTracker=VelocityTracker.get()
}
mVelocityTracker!!.addMovement(ev)
val动作=ev动作
valy=ev.y
val x=ev.x
何时(行动){
MotionEvent.ACTION\u向下->{
/*
*如果被投掷并且用户触摸,停止投掷。isFinished
*将会失败