自定义沙盘

需求

这是一个很早之前的需求了,之前没有发博客的习惯,最近想做一个回顾,也是为了做一个小小的总结把,把之前做的一些东西,和碰到的一些问题,总体的回顾一下,反思一下。因为从事房地产相关的工作,所以需要实现一个沙盘的功能,让客户可以在手机上查看相关的楼栋信息,在网上找了两天,没有找到类似的,只能自己硬着头皮写了,当时写的时候整整花了一个星期,也是这方面的接触实在太少,一些炫酷的东西从来都是拿来主义,都是抱着网上能找到的东西从来不会动脑子自己去弄,不过每次写完类似的东西,心里还是挺开心的,扯了这么多闲篇,进入正题。

 

-效果图

自定义沙盘_第1张图片

 

思路

1. 了解了需求之后,其实有点头皮发麻,脑子里出现的第一个想法就是上网找,结果遗憾收场!然后只能自己分析了,首先需要显示一张沙盘的图片,然后图片可以缩放,可以拖拽,想到这里这不是经常用的相册里面的大图预览的功能吗,果断打开项目看了用到的用于大图浏览的框架,缩放拖拽的功能都有,但是怎么加标注,而且标注还要随着图片的改变而变化位置,放弃了这个思路。清一清脑子,看到同事在开发地图相关的功能,脑子一亮?哈哈,这个不就是类似地图的一小部分功能吗,然后写了个demo集成了百度地图的sdk,往上添加标注,果然需要的就是这个!但是不能直接用地图的功能,不过好歹有了个思路,后面通过验证,确实可行。

2. 确定实现方案:首先需要确定一些关键点,图片不能被裁剪,只能被等比例缩放,所以我在FramLayout里面又放了一个FramLayout,作为容器来放图片,和需要放到特定位置的标注。拖拽和缩放的都是里面的一个容器FramLayout,确定了思路接下来就是开始码代码了。

准备工作

真正开始编码之前需要做一些必须的准备工作,那就是确定缩放、拖拽的范围,还有就是关于标注的位置超界的判定等等。这时候画几个图就一目了然了。

自定义沙盘_第2张图片


 

自定义沙盘_第3张图片

 

自定义沙盘_第4张图片

开发之前画一些类似的图可以让我们开发的时候脑子更清晰,一开始我是没有画的,结果写的时候脑子越来越糊涂!!!

代码实现



/**
 * @author :Chenqi
 * 

* date :2018/5/23 下午2:37 * description :自定义沙盘View */ @SuppressWarnings("unused") public class SandTabView extends FrameLayout implements ScaleGestureDetector.OnScaleGestureListener { //点击事件的时间判定值 private static final int CLICK_STANDARD_TIME = 200; //最大放大倍数 private static final int SCALE_MAX_VALUE = Integer.MAX_VALUE; //声明拖拽帮助类 private ViewDragHelper mDragHelper; //声明缩放手势手势监听 private ScaleGestureDetector mScaleGestureDetector; //声明沙盘图、标注的容器 private FrameLayout mContentView; //声明手指按下时间 private long mPointDownTimeStamp; //声明触摸事件类型 private int mTouchMode; //声明缩放事件类型 private int mScaleMode; //记录开始触摸前两指距离 private float mPointDistanceBeforeScale; //当前缩放比 private float mCurScaleFactor = 1; //总缩放比 private float mAllScaleFactor = 1; //初始缩放比 private float mOriginalFactor; //单击按下坐标 private PointF mPointDown = new PointF(); //单击抬起坐标 private PointF mPointUp = new PointF(); //声明缩放中心点 private PointF mScaleCenPoint = new PointF(); //缩放前距左,距右边距 private PointF mPointLeftAndTop = new PointF(); //开始缩放前标注父容器位置记录 private Rect mOriginRect = new Rect(); //声明初始加载的View大小 private PointF mViewOriginSize; //点击回调 private ISandTabItemClick mISandTabItemClick; //加载回调 private ISandTabLoadCallBack mLoadCallBack; //是否可以点击 private boolean mIsTouchEnable = true; //是否已经生成图片展示 private boolean mHaveViewCut = false; //第一次加载是否完成 private boolean mFirstLoaded = false; public SandTabView(@NonNull Context context) { this(context, null); } public SandTabView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } public SandTabView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public void setISandTabItemClick(ISandTabItemClick ISandTabItemClick) { mISandTabItemClick = ISandTabItemClick; } public void setTouchEnable(boolean touchEnable) { mIsTouchEnable = touchEnable; } private void init() { //初始化拖拽帮助类 mDragHelper = ViewDragHelper.create(this, 2f, new ViewDragCallBack()); //初始化缩放手势监听 mScaleGestureDetector = new ScaleGestureDetector(getContext(), this); //初始化容器 mContentView = new FrameLayout(getContext()); //添加容器 addView(mContentView); LayoutParams layoutParams = (LayoutParams) mContentView.getLayoutParams(); mContentView.setLayoutParams(layoutParams); } /** * 设置沙盘图片资源 * * @param url:图片地址 */ public void setImagePath(String url, ISandTabLoadCallBack loadCallBack) { mLoadCallBack = loadCallBack; Glide.with(getContext()).load(url).into(new SimpleTarget() { @Override public void onResourceReady(@NonNull Drawable resource, @Nullable Transition transition) { final Drawable sandTabDrawable = resource; mContentView.post(() -> { mViewOriginSize = CalUtil.calScalePictureSize(SandTabView.this, sandTabDrawable); mOriginalFactor = mViewOriginSize.x / resource.getIntrinsicWidth(); mAllScaleFactor = mOriginalFactor; LayoutParams contentViewLayoutParams = (LayoutParams) mContentView.getLayoutParams(); contentViewLayoutParams.width = (int) mViewOriginSize.x; contentViewLayoutParams.height = (int) mViewOriginSize.y; mContentView.setLayoutParams(contentViewLayoutParams); mContentView.setBackground(sandTabDrawable); if (mLoadCallBack != null) mLoadCallBack.onBgLoaded(); }); } }); } /** * 添加标注View * * @param markers:标注View集合 */ public void addMarkers(List markers) { if (markers == null) return; mContentView.removeAllViews(); for (int i = 0; i < markers.size(); i++) { BaseSandTabMarker marker = markers.get(i); mContentView.addView(marker); LayoutParams layoutParams = (LayoutParams) marker.getLayoutParams(); layoutParams.leftMargin = Math.round(marker.xAxis() * mAllScaleFactor) - marker.getCenterLeft(); layoutParams.topMargin = Math.round(marker.yAxis() * mAllScaleFactor) - marker.getCenterTop(); layoutParams.width = marker.measureWidth(); layoutParams.height = marker.measureHeight(); marker.setLayoutParams(layoutParams); if (i == markers.size() - 1) { marker.getViewTreeObserver().addOnGlobalLayoutListener(() -> { if (mLoadCallBack != null && !mFirstLoaded) { mLoadCallBack.onMarkerLoaded(); mFirstLoaded = true; } }); } } } /** * 设置当前显示的marker * * @param position:目标marker下标 */ public void selectMarker(int position, boolean isSelect) { if (mContentView != null && mContentView.getChildCount() > 0) scrollToMarker(position, isSelect); } /** * 截图控件内容后生成图片展示并移除所有子控件 */ public void cutViewContent() { if (mHaveViewCut) return; destroyDrawingCache(); setDrawingCacheEnabled(true); buildDrawingCache(); setBackground(new BitmapDrawable(getResources(), getDrawingCache())); removeAllViews(); mHaveViewCut = true; } @Override public boolean performClick() { if (mContentView != null) for (int i = 0; i < mContentView.getChildCount(); i++) { BaseSandTabMarker childAt = (BaseSandTabMarker) mContentView.getChildAt(i); int correctX = Math.round((mPointDown.x + Math.abs(mContentView.getLeft()))); int correctY = Math.round(mPointDown.y + Math.abs(mContentView.getTop())); if (correctX < childAt.getRight() && correctX > childAt.getLeft() && correctY > childAt.getTop() && correctY < childAt.getBottom() && mISandTabItemClick != null) { mISandTabItemClick.onMarkerItemClick(i); scrollToMarker(i, true); break; } } return super.performClick(); } /** * 将marker移动到控件中心点 * * @param position:目标Marker对应下标 */ private void scrollToMarker(int position, boolean isSelect) { //重置marker状态 if (isSelect) for (int i = 0; i < mContentView.getChildCount(); i++) { BaseSandTabMarker marker = (BaseSandTabMarker) mContentView.getChildAt(i); marker.select(i == position); } //计算child相对于父容器的父容器的位置 BaseSandTabMarker child = (BaseSandTabMarker) mContentView.getChildAt(position); if (child == null) return; int childCenterX = child.getLeft() - Math.abs(mContentView.getLeft()) + child.getWidth() / 2; int childCenterY = child.getTop() - Math.abs(mContentView.getTop()) + child.getHeight() / 2; //child中心点横纵坐标相对于控件中心点的差值 int deltaX = getWidth() / 2 - childCenterX; int deltaY = getHeight() / 2 - childCenterY; //最终标注View载体的左上位置 int finalLeft = mContentView.getLeft() + deltaX; int finalTop = mContentView.getTop() + deltaY; //左边界判断 finalLeft = finalLeft > 0 ? 0 : finalLeft; //右边界判断 if (finalLeft + mContentView.getWidth() < getWidth()) finalLeft = getWidth() - mContentView.getWidth(); //上边界判断 finalTop = finalTop > 0 ? 0 : finalTop; //下边界判断 if (finalTop + mContentView.getHeight() < getHeight()) finalTop = getHeight() - mContentView.getHeight(); mDragHelper.smoothSlideViewTo(mContentView, finalLeft, finalTop); ViewCompat.postInvalidateOnAnimation(SandTabView.this); } @Override public void computeScroll() { super.computeScroll(); if (mDragHelper.continueSettling(true)) { ViewCompat.postInvalidateOnAnimation(SandTabView.this); } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mDragHelper.shouldInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { if (!mHaveViewCut && mIsTouchEnable) switch (event.getAction() & event.getActionMasked()) { case MotionEvent.ACTION_DOWN: mTouchMode = TOUCH_MODE_DRAG; //记录点击的时间 mPointDownTimeStamp = System.currentTimeMillis(); //记录单击坐标 mPointDown.set(event.getX(), event.getY()); break; case MotionEvent.ACTION_POINTER_DOWN: mTouchMode = TOUCH_MODE_SCALE; mPointDistanceBeforeScale = CalUtil.calDistanceOf2Point( event.getX(0), event.getY(0), event.getX(1), event.getY(1)); //记录手指缩放中心点 mScaleCenPoint.set( (event.getX(1) + event.getX(0)) / 2, (event.getY(1) + event.getY(0)) / 2); mOriginRect.set(mContentView.getLeft(), mContentView.getTop(), mContentView.getRight(), mContentView.getBottom()); break; case MotionEvent.ACTION_MOVE: if (event.getPointerCount() > 1 && mTouchMode == TOUCH_MODE_SCALE) { mScaleMode = CalUtil.calDistanceOf2Point( event.getX(0), event.getY(0), event.getX(1), event.getY(1)) > mPointDistanceBeforeScale ? SCALE_MODE_ENLARGE : SCALE_MODE_NARROW; } mPointLeftAndTop.set(mContentView.getLeft(), mContentView.getTop()); break; case MotionEvent.ACTION_UP: //获取手指抬起的时间 mPointUp.set(event.getX(), event.getY()); long pointUpTimeStamp = System.currentTimeMillis(); long deltaTimeStamp = pointUpTimeStamp - mPointDownTimeStamp; if (deltaTimeStamp <= CLICK_STANDARD_TIME) performClick(); break; default: break; } //分发事件 switch (mTouchMode) { case TOUCH_MODE_DRAG: mDragHelper.processTouchEvent(event); break; case TOUCH_MODE_SCALE: mScaleGestureDetector.onTouchEvent(event); break; default: break; } return true; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (mTouchMode == TOUCH_MODE_SCALE) { //缩放后重新设定位置 int leftAfterScale; int topAfterScale; //缩放后的left = 缩放前left * 缩放率(重新定位) - 缩放中心点的偏移量 if (mScaleMode == SCALE_MODE_ENLARGE) { leftAfterScale = (int) (mPointLeftAndTop.x * mCurScaleFactor - mScaleCenPoint.x * (mCurScaleFactor - 1)); topAfterScale = (int) (mPointLeftAndTop.y * mCurScaleFactor - mScaleCenPoint.y * (mCurScaleFactor - 1)); } else { leftAfterScale = (int) (mPointLeftAndTop.x * mCurScaleFactor + mScaleCenPoint.x * (1 - mCurScaleFactor)); topAfterScale = (int) (mPointLeftAndTop.y * mCurScaleFactor + mScaleCenPoint.y * (1 - mCurScaleFactor)); } int rightAfterScale = leftAfterScale + mContentView.getWidth(); int bottomAfterScale = topAfterScale + mContentView.getHeight(); //防止缩放时出现白边,进行边界判断,保证缩放在控件内进行。 if (leftAfterScale > 0) { leftAfterScale = 0; rightAfterScale = mContentView.getWidth(); } if (topAfterScale > 0) { topAfterScale = 0; bottomAfterScale = mContentView.getHeight(); } if (rightAfterScale < getWidth()) { rightAfterScale = getWidth(); leftAfterScale = getWidth() - mContentView.getWidth(); } if (bottomAfterScale < getHeight()) { bottomAfterScale = getHeight(); topAfterScale = getHeight() - mContentView.getHeight(); } mContentView.layout(leftAfterScale, topAfterScale, rightAfterScale, bottomAfterScale); reLayoutMarkers(); } } /** * 标注重新布局 */ private void reLayoutMarkers() { for (int i = 0; i < mContentView.getChildCount(); i++) { BaseSandTabMarker child = (BaseSandTabMarker) mContentView.getChildAt(i); int left = Math.round(child.xAxis() * mAllScaleFactor) - child.getCenterLeft(); int top = Math.round(child.yAxis() * mAllScaleFactor) - child.getCenterTop(); child.layout(left, top, left + child.getWidth(), top + child.getHeight()); } } /** * 检查缩放范围 * * @return :是否可以缩放 */ private boolean checkScaleRange() { return mScaleMode == SCALE_MODE_ENLARGE && mContentView.getWidth() / mViewOriginSize.x < SCALE_MAX_VALUE && mContentView.getHeight() / mViewOriginSize.y < SCALE_MAX_VALUE || mScaleMode == SCALE_MODE_NARROW && mContentView.getWidth() / mViewOriginSize.x > 1 && mContentView.getHeight() / mViewOriginSize.y > 1; } /** * 开始缩放 * * @param detector :缩放手势监听 * @return :是否可以缩放 */ @Override public boolean onScaleBegin(ScaleGestureDetector detector) { return checkScaleRange(); } /** * 进行缩放 * * @param detector :缩放手势监听 * @return :返回true可进行缩放 */ @Override public boolean onScale(ScaleGestureDetector detector) { mCurScaleFactor = detector.getScaleFactor(); if (checkScaleRange()) { float newWidth = mContentView.getWidth() * mCurScaleFactor; LayoutParams layoutParams = (LayoutParams) mContentView.getLayoutParams(); layoutParams.width = Math.round(mContentView.getWidth() * mCurScaleFactor); layoutParams.height = Math.round(mContentView.getHeight() * mCurScaleFactor); mAllScaleFactor *= mCurScaleFactor; if (newWidth < mViewOriginSize.x) { mCurScaleFactor = mViewOriginSize.x / newWidth; mAllScaleFactor = mOriginalFactor; layoutParams.width = Math.round(mViewOriginSize.x); layoutParams.height = Math.round(mViewOriginSize.y); layoutParams.setMargins(mOriginRect.left, mOriginRect.top, mOriginRect.right, mOriginRect.bottom); } mContentView.setLayoutParams(layoutParams); } return true; } /** * 缩放结束回调 * * @param detector :缩放手势监听 */ @Override public void onScaleEnd(ScaleGestureDetector detector) { } /** * 拖拽回调 */ private class ViewDragCallBack extends ViewDragHelper.Callback { /** * 尝试捕捉目标(拖动的)视图 * * @param child :子视图 * @param pointerId :正在被拖动捕获的子视图的指针Id * @return :可以被拖动的子View */ @Override public boolean tryCaptureView(@NonNull View child, int pointerId) { return mContentView != null && child == mContentView && mTouchMode == TOUCH_MODE_DRAG; } /** * 子视图横向拖拽的距离监听 * * @param child :子视图 * @param left :应该拖动到x轴坐标的位置 * @param dx :横向拖动的距离 * @return :横向应该拖动的距离 */ @Override public int clampViewPositionHorizontal(@NonNull View child, int left, int dx) { //横向拖动边界判断 if (left > 0) left = 0; if (left < (getWidth() - child.getWidth())) left = getWidth() - child.getWidth(); return left; } /** * 子视图纵向拖动的距离监听 * * @param child :子控件 * @param top :应该拖动到y轴坐标的位置 * @param dy :纵向拖动的距离 * @return :纵向应该拖动的距离 */ @Override public int clampViewPositionVertical(@NonNull View child, int top, int dy) { //纵向拖动边界判断 if (top > 0) top = 0; if (top < (getHeight() - child.getHeight())) top = getHeight() - child.getHeight(); return top; } } }

这是实现的基本的代码,完整代码可以到git上查看,地址:https://github.com/Cq1993/SandTab.git,代码上都有清楚的注释。

你可能感兴趣的:(沙盘,自定义控件,SandTabView,Android,房地产,自定义控件)