自定义控件实例(一) 仿QQ小红点

最近在复习Android,重新捡起自定义控件。
总结一句话,不要用小聪明规避问题,不然你下次遇到了,你还是什么都不懂。
先看效果:
在这里插入图片描述
工程以及说明: QQ小红点

首先,看到这个,想到的就是使用贝塞尔曲线了。如果你对怎么绘制不了解,可以先通过启舰大佬,了解QQ小红点怎么实现的。
自定义控件三部曲之绘图篇(十五)——QQ红点拖动删除效果实现(基本原理篇)

不过上面的文章,只能在view级别上使用,就算使用了 clipchild = false,也无法夸层级绘制的,所以这篇文章是在此基础上,让它可以在所有view上都能使用。
思路如下:

  • 用一个BezierPointView继承TextView,拿到自定义配置参数,比如大小,颜色
  • 在 BezierPointView 的Down事件时,把QQ小红点 (BezierPointWindow) 通过 Windowmanager 添加进来,再把 BezierPointView 的visiable 设置 gone
  • 当 BezierPointWindow 的Up 事件时,把坐标传递回来,然后移除BezierPointWindow,添加一个ImageView,再在此做自定义的属性动画,或者默认的图片爆炸效果
  • 移除windowmanager,让 BezierPointView 显示出来即可。

这里我没有跟启舰大佬那样,用viewgroup来实现,其实用自定义 View 即可实现。实现的原理,启舰大佬已经讲解得很清楚了
在这里插入图片描述
我们只要找准一个方向,计算切线即可。如果两个 a 相等呢?相信你的判断两条线是否平行的原理还记得。
既然知道了两个小球的坐标和半径,那么切点的坐标也很好理解了,然后再计算不断变化的半径和最大断开的长度即可。

    /**
     * 计算贝塞尔曲线
     * 由于Math 的扫脚函数,都自带正负的,所以,只需要计算一种方向即可
     * 这里的计算方向为右下
     */
    private void calculateBeizer() {
        float x0 = mStartPoint.x;
        float y0 = mStartPoint.y;
        float x = mMovePoint.x;
        float y = mMovePoint.y;

        //算出夹角
        float dx = x - x0;
        float dy = y - y0;
        double a = Math.atan(dy / dx);
        //拿到圆切点的长度偏移量
        float offsetx0 = (float) (mDrawRadius * Math.sin(a));
        float offsety0 = (float) (mDrawRadius * Math.cos(a));
        
        //拿到第二个小球的偏移量
        float offsetx = (float) (mDefaultRadius * Math.sin(a));
        float offsety = (float) (mDefaultRadius * Math.cos(a));

        //算出第一个圆的切点坐标
        float p0x = x0 + offsetx0;
        float p0y = y0 - offsety0;

        float p1x = x0 - offsetx0;
        float p1y = y0 + offsety0;
        //算出第二个圆的切点坐标
        float p2x = x + offsetx;
        float p2y = y - offsety;

        float p3x = x - offsetx;
        float p3y = y + offsety;
        //计算贝塞尔辅助点
        float anchorx = (x0 + x) / 2;
        float anchory = (y0 + y) / 2;
        //清掉上次,避免残留
        mPath.reset();

        //形成贝塞尔曲线
        mPath.moveTo(p0x, p0y);
        mPath.quadTo(anchorx, anchory, p2x, p2y);
        mPath.lineTo(p3x, p3y);
        mPath.quadTo(anchorx, anchory, p1x, p1y);
        mPath.close();

        //超过一定距离时,且圆形的半径也要跟着变小
        double distance = getDistance(mMovePoint, mStartPoint);
        mDrawRadius = (int) (mDefaultRadius - distance / 12);
        if (mDrawRadius <= 7) {
            mDrawRadius = 7;
        }
        if (distance >= mMaxMoveLength) {
            // 超过一定距离 贝塞尔和固定圆都不要画了
            mIsBreakUp = true;
            mDrawRadius = 0;
            mPath.reset();
            return;
        }
        
    }

在所有 View 上使用

我们可以通过一个 BezierPointView 拿到自定义属性之后,在 onSizeChange 的时候,配置windowmanger:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mPointHelper = new PointHelper(this);
        setOnTouchListener(mPointHelper);
    }

而PointHelper 其实是一个 windowmanager 的工具类,继承View.OnTouchListener,这样就可以在 onTouch 方法中,添加windowmanager :

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        ViewParent parent = v.getParent();
        if (parent == null) {
            return false;
        }
        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {

            if (!mWindowView.isCanMove()) {
                return false;
            }
            //接管父控件的touch事件
            parent.requestDisallowInterceptTouchEvent(true);

            /**
             *  当按下时,再把View添加到windowmanager,此时的windowmanager是全屏的;
             *  所以,pointview的操作不会因为控件大小的限制,导致绘制不全的问题
             */

            addPointToWindow();
        }
        return mWindowView.onTouchEvent(event);

可以看到 touch之后是交给 BezierPointWindow 的touch 事件,而addPointToWindow 为具体的添加效果。


    /**
     * 把TextView 的缓存bitmap给BezierPointWindow绘制
     */
    private void addPointToWindow() {

        if (mContainer == null) {
            mContainer = new FrameLayout(mContext);
            mContainer.setClipChildren(false);
            mContainer.setClipToPadding(false);
            mWindowView.setLayoutParams(mLayoutParams);
        }
        mContainer.removeAllViews();
        mContainer.addView(mWindowView, mParams);
        mWindowManager.addView(mContainer, mParams);
        //初始化坐标
        mPointView.getLocationInWindow(mPos);
        int width = mPointView.getWidth();
        int height = mPointView.getHeight();

        //设置大小和起始位置
        mWindowView.initPoint(mPos[0] + width / 2, mPos[1] + height / 2);
        mWindowView.setPointListener(this);
        mWindowView.setVisibility(View.VISIBLE);
        //拿到bitmap
        mPointView.setDrawingCacheEnabled(true);
        //拿到TextView的缓存bitmap,给BezierPointWindow 绘制,也为后面up的 imageview 当做背景图
        Bitmap bitmap = Bitmap.createBitmap(mPointView.getDrawingCache());
        mPointView.setDrawingCacheEnabled(false);
        mWindowView.setBitmap(bitmap);
    }

为了避免闪烁问题,我们需要当 BezierPointWindow 绘制好 bitmap 之后,再让BezierPointView 消失:

    @Override
    public void onDrawReady() {
        //这个时候,可以消失了,避免闪烁
        mPointView.setVisibility(View.GONE);
    }

而在 up 之后,之后,只需要添加一个 ImageView,实现要消失的动画即可:

    @Override
    public void destroy(PointF pointF) {
        //移除所有
        mContainer.removeAllViews();
        //添加imageview,用于动画
        mContainer.addView(mImageView, mLayoutParams);
        //指定初始位置
        mImageView.setX(pointF.x - mPointView.getWidth() * 1.0f / 2);
        mImageView.setY(pointF.y - mPointView.getHeight() * 1.0f / 2);

        if (mPointView.isUseSelfAnim()){
            //使用自定义动画
            mImageView.setImageBitmap(mWindowView.getBitmap());
            if (mPointView.getListener() != null) {
                mPointView.getListener().destory(mImageView);
            }
        }else{
            //使用布局标签的属性动画
           if (mPointView.getAnimatorRes() != -1){
                mImageView.setImageBitmap(mWindowView.getBitmap());
                mAnimator = AnimatorInflater.loadAnimator(mContext, mPointView.getAnimatorRes());
                mAnimator.setTarget(mImageView);
                mAnimator.start();
                mAnimator.addListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        removeView();
                    }
                });
            }else{
                //使用默认动画,即图片爆炸
                mImageView.setBackgroundResource(R.drawable.anim_blow);
                mAnimationDrawable = (AnimationDrawable) mImageView.getBackground();
                mAnimationDrawable.start();
                mImageView.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        removeView();
                    }
                },getAnimationDrawableTime(mAnimationDrawable));
            }
        }

    }

需要注意的是,最后需要使用 removeView 方法,不然这层 windowmanager 的view 不会消失:

    /**
     * 移除View,这个必须要实现,否则就有一层view在顶层,啥也操作不了
     */
    public void removeView() {
        if (mWindowManager != null) {
            if (mContainer != null && mContainer.isAttachedToWindow()) {
                mWindowManager.removeView(mContainer);
            }
        }
        //还原set属性,防止view被设置动画,初始位置错乱
        mWindowView.clearAnimation();
        mWindowView.setAlpha(1);
        mWindowView.setX(0);
        mWindowView.setY(0);
        mWindowView.setScaleX(1);
        mWindowView.setScaleY(1);
        mWindowView.setVisibility(View.GONE);
        if (mContainer != null && mContainer.getChildCount() > 0) {
            mContainer.removeView(mWindowView);
        }
    }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: WPF(Windows Presentation Foundation)是一种用于创建功能丰富的用户界面的技术,它提供了许多预定义的控件,但也支持自定义控件来满足更特定的需求。 在WPF中,我们可以通过扩展现有的控件或创建全新的控件来自定义控件。具体步骤如下: 1. 创建一个新的类,继承自WPF中的控件类(如Button、TextBox等),这个新类将成为我们自定义控件的基类。 2. 在新类中添加需要的属性、方法和事件,以满足我们的特定需求。我们可以根据需要自定义控件的外观、行为和逻辑。 3. 使用WPF的外观系统来定义自定义控件的视觉效果。可以使用XAML来定义控件的样式、模板和触发器,也可以通过代码动态创建控件。 4. 在控件的构造函数中初始化控件的默认属性和事件。 5. 可选地,可以实现控件的依赖属性,这样就可以通过绑定的方式将数据与控件关联起来。 6. 在需要使用自定义控件的地方,将控件添加到XAML布局或通过代码创建并添加到视觉树中。 通过自定义控件,我们可以实现更灵活、更具个性化的用户界面,同时提供更好的用户体验。自定义控件可以适应各种复杂的场景和需求,从而提供更具创意和创新性的用户界面设计。 总结来说,使用WPF自定义控件的过程包括创建扩展自控件基类、添加属性和事件、定义外观效果、初始化属性和事件、实现依赖属性等步骤。自定义控件能够满足特定需求,提供更灵活、个性化的用户界面。 ### 回答2: WPF(Windows Presentation Foundation)是微软推出的一种优秀的用户界面开发技术,允许开发者创建高度可定制的界面。而自定义控件则是WPF中的一种重要功能,它允许开发者根据自己的需求创建全新的用户界面元素。 自定义控件实例如下: 我们假设要创建一个自定义按钮控件,该按钮有独特的外观和交互行为。首先,我们需要在WPF应用程序中定义一个新的自定义控件类,该类继承自Button类。然后,我们可以在控件类中添加新的依赖属性以实现更多的定制化选项。 在控件类中,我们可以重写OnApplyTemplate方法以定义在控件模板中使用的可视化元素。比如,我们可以定义一个Grid作为按钮的视觉部分,并在Grid中添加一个Border作为按钮的背景。还可以添加鼠标事件处理程序以响应用户的交互动作。 接下来,我们需要在XAML文件中使用我们自定义的按钮控件。我们可以在Window或者其他容器中引用自定义按钮,并设置其属性和事件处理程序。比如,我们可以设置按钮的背景颜色为蓝色,文本为“点击我”,并为按钮的Click事件添加一个处理方法。 通过这种方式,我们可以实现一个具有独特外观和交互行为的自定义按钮控件。开发者可以根据自己的需求定义更多的自定义控件,从而为用户提供更灵活和丰富的界面体验。 总而言之,WPF中的自定义控件是一种强大的功能,它允许开发者创建全新的用户界面元素,实现更高度的定制化和交互行为。通过合理地使用自定义控件,开发者可以为用户提供更好的用户界面体验。 ### 回答3: WPF(Windows Presentation Foundation)是一个用于创建 Windows 桌面应用程序的开发框架,它提供了丰富的图形化用户界面(GUI)设计工具和功能。在 WPF 中,我们可以创建自定义控件来满足特定的需求。 自定义控件是指在原有的 WPF 控件基础上进行扩展或重写,以满足特定场景的需求。通过自定义控件,我们可以实现更加灵活的界面设计和交互方式。 创建自定义控件的过程包括以下几个步骤: 1. 继承现有的 WPF 控件:可以选择一个现有的 WPF 控件作为基类,然后通过扩展或重写其功能来实现自定义控件。例如,我们可以继承 Button 控件,并添加一些额外的属性和事件来实现一个特定的按钮效果。 2. 添加依赖属性:依赖属性是 WPF 中一种特殊的属性,它可以提供数据的绑定和通知机制。通过添加依赖属性,我们可以在自定义控件中定义可以被外部代码修改和引用的属性。 3. 创建控件模板:控件模板定义了控件的外观和布局方式。可以通过 XAML 或代码方式创建控件模板,并将其应用到自定义控件中。 4. 添加样式和模板绑定:可以为自定义控件添加样式,定义其在不同状态下的外观效果。同时,可以通过模板绑定将控件属性和模板中的元素进行关联。 创建完成后,我们可以在 XAML 中使用自定义控件,就像使用任何其他的 WPF 控件一样。可以设置自定义控件的属性、订阅事件,并将其嵌入到应用程序的界面中。 总的来说,WPF 的自定义控件功能可以帮助开发者实现更加灵活和个性化的用户界面设计。通过继承和扩展现有的控件,添加依赖属性和控件模板,我们可以创建出符合特定需求的自定义控件,并在应用程序中灵活使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值