以编程方式绘制气泡

时间:2013-12-27 19:32:22

标签: android android-canvas

我想在我的应用中有一个带有百分比值的气泡,我不能使用9个补丁,因为我希望它可以自定义,其背景颜色应该是可变的。 看起来应该是这样的 enter image description here

我该怎么办?这个泡泡会在其内部膨胀视图,如此百分比或一些较大的布局。 另外,根据布局(手机或平板电脑),它可能有一侧比另一侧大(箭头不在中心),这是我更喜欢以编程方式进行操作的另一个原因

2 个答案:

答案 0 :(得分:15)

创建一个自定义Drawable并将其用于放置文本或其他视图的任何容器的背景。
您需要修改背景的填充以考虑气泡的指针 下面的代码允许您将指针的对齐方式设置为LEFT,CENTER或RIGHT 这只是一个给你一个想法的基本版本。您可以轻松地为气泡颜色添加设置器,或者为'mPaint'添加描边属性以获得额外的灵活性。

public class BubbleDrawable extends Drawable {

    // Public Class Constants
    ////////////////////////////////////////////////////////////

    public static final int LEFT = 0;
    public static final int CENTER = 1;
    public static final int RIGHT = 2;

    // Private Instance Variables
    ////////////////////////////////////////////////////////////

    private Paint mPaint;
    private int mColor;

    private RectF mBoxRect;
    private int mBoxWidth;
    private int mBoxHeight;
    private float mCornerRad;
    private Rect mBoxPadding = new Rect();

    private Path mPointer;
    private int mPointerWidth;
    private int mPointerHeight;
    private int mPointerAlignment;

    // Constructors
    ////////////////////////////////////////////////////////////

    public BubbleDrawable(int pointerAlignment) {
        setPointerAlignment(pointerAlignment);
        initBubble();
    }

    // Setters
    ////////////////////////////////////////////////////////////

    public void setPadding(int left, int top, int right, int bottom) {
        mBoxPadding.left = left;
        mBoxPadding.top = top;
        mBoxPadding.right = right;
        mBoxPadding.bottom = bottom;
    }

    public void setCornerRadius(float cornerRad) {
        mCornerRad = cornerRad;
    }

    public void setPointerAlignment(int pointerAlignment) {
        if (pointerAlignment < 0 || pointerAlignment > 3) {
            Log.e("BubbleDrawable", "Invalid pointerAlignment argument");
        } else {
            mPointerAlignment = pointerAlignment;
        }
    }

    public void setPointerWidth(int pointerWidth) {
        mPointerWidth = pointerWidth;
    }

    public void setPointerHeight(int pointerHeight) {
        mPointerHeight = pointerHeight;
    }

    // Private Methods
    ////////////////////////////////////////////////////////////

    private void initBubble() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mColor = Color.RED;
        mPaint.setColor(mColor);
        mCornerRad = 0;
        setPointerWidth(40);
        setPointerHeight(40);
    }

    private void updatePointerPath() {
        mPointer = new Path();
        mPointer.setFillType(Path.FillType.EVEN_ODD);

        // Set the starting point
        mPointer.moveTo(pointerHorizontalStart(), mBoxHeight);

        // Define the lines
        mPointer.rLineTo(mPointerWidth, 0);
        mPointer.rLineTo(-(mPointerWidth / 2), mPointerHeight);
        mPointer.rLineTo(-(mPointerWidth / 2), -mPointerHeight);
        mPointer.close();
    }

    private float pointerHorizontalStart() {
        float x = 0;
        switch (mPointerAlignment) {
        case LEFT:
            x = mCornerRad;
            break;
        case CENTER:
            x = (mBoxWidth / 2) - (mPointerWidth / 2);
            break;
        case RIGHT:
            x = mBoxWidth - mCornerRad - mPointerWidth;
        }
        return x;
    }

    // Superclass Override Methods
    ////////////////////////////////////////////////////////////

    @Override
    public void draw(Canvas canvas) {
        mBoxRect = new RectF(0.0f, 0.0f, mBoxWidth, mBoxHeight);
        canvas.drawRoundRect(mBoxRect, mCornerRad, mCornerRad, mPaint);
        updatePointerPath();
        canvas.drawPath(mPointer, mPaint);
    }

    @Override
    public int getOpacity() {
        return 255;
    }

    @Override
    public void setAlpha(int alpha) {
        // TODO Auto-generated method stub

    }

    @Override
    public void setColorFilter(ColorFilter cf) {
        // TODO Auto-generated method stub

    }

    @Override
    public boolean getPadding(Rect padding) {
        padding.set(mBoxPadding);

        // Adjust the padding to include the height of the pointer
        padding.bottom += mPointerHeight;
        return true;
    }

    @Override
    protected void onBoundsChange(Rect bounds) {
        mBoxWidth = bounds.width();
        mBoxHeight = getBounds().height() - mPointerHeight;
        super.onBoundsChange(bounds);
    }
}

<强>用法
下面的示例显示了如何使用BubbleDrawable。

<强> MainActivity.java

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        LinearLayout linearLayout = (LinearLayout)findViewById(R.id.myLayout);
        BubbleDrawable myBubble = new BubbleDrawable(BubbleDrawable.CENTER);
        myBubble.setCornerRadius(20);
        myBubble.setPointerAlignment(BubbleDrawable.RIGHT);
        myBubble.setPadding(25, 25, 25, 25);
        linearLayout.setBackgroundDrawable(myBubble);
    }
}

<强> activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <LinearLayout
        android:id="@+id/myLayout"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true" >

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Some Text"
            android:textAppearance="?android:attr/textAppearanceLarge" />

        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Some Other Text"
            android:textAppearance="?android:attr/textAppearanceLarge" />
        </LinearLayout>

</RelativeLayout>

答案 1 :(得分:13)

显然,在你的应用程序中使用你不理解的代码永远不是一个好主意,所以我不会只为你编写java代码中的一堆方程式。但是,如果您遵循并理解下面的数学,那么在代码中使用所描述的方程并绘制圆弧将是一个相对简单的事情。


要在指针上获得圆形笔尖,您需要修改updatePointerPath() 目前它只是使用rLineTo()绘制三条线形成一个三角形 android Path类中有另一种名为arcTo()的方法,它采用以下形式:

    arcTo(RectF oval, float startAngle, float sweepAngle)

您可以使用此方法在指针的尖端绘制圆弧,但您需要首先计算一些内容。

您已经可以计算指针三角形三个角的坐标。这就是updatePointerPath()已经做过的事情。现在看一下下图。要使用arcTo(),您需要计算以下内容:

  1. T 的坐标,这是弧开始的位置。
  2. 边界RectF的左上角和右下角的坐标
  3. 您的起始角度(Alpha
  4. 您的扫掠角度(2 * Phi
  5. Diagram 2

    如下图所示,可以使用基本触发器轻松找到起始角度Alpha

    Diagram 1

    注意:最好是坚持使用Radians而不是Degrees用于所有角度,因为这是android'Math'类中所有trig函数所需要的。
    考虑到这一点:

    • 圆圈中有2个Pi弧度
    • 三角形中的三个角度加起来为Pi Radians
    • 直角为Pi / 2弧度

    因此,在点 C T P 形成的三角形中添加三个角度,您得到:

    Alpha + Phi + Pi / 2 = Pi

    因此

    Phi = Pi / 2 - Alpha

    所以我们现在计算了AlphaPhi

    接下来, d 是点 P 与边界框底部之间的距离。
    您可以通过计算从 C 点到 P 点的距离,然后减去半径 r 来获得它。
    现在:

    罪(Alpha)= r /(从 C P 的距离)

    因此:

    C P = r / sin(Alpha)的距离

    因此,距离 d 是从点 C 到点 P 的距离减去半径 r ,我们得到:

    d =( r / sin(Alpha)) - r

    它为您提供了计算边界RectF左上角和右下角坐标所需的所有信息。

    现在剩下的就是找出点 T 的坐标。

    首先计算从 P T 的距离 鉴于:

    tan(Alpha)= r /(距 P T 的距离)

    我们得到:

    P T = r / tan(Alpha)的距离

    最后,在图表中添加一个点......

    Diagram 3

    我们可以看到:

    罪(Alpha)=(从 P A 的距离)/(从 P T的距离

    所以:

    P A 的距离=(从 P T 的距离)* sin({{0 }})

    类似地:

    cos(Alpha)=(从 T A 的距离)/(从 P T的距离

    所以:

    T A 的距离=(从 P T 的距离)* cos({{0 }})

    使用此信息,您现在可以计算点 T 的坐标!!

    如果你理解了这一切,那么这里的编码很容易。如果你不确定什么,那就问问。


    以下介绍了更新后的updatePointerPath()的外观。

    private void updatePointerPath() {
        float xDistance;
        float yDistance;
        mPointer = new Path();
        mPointer.setFillType(Path.FillType.EVEN_ODD);
    
        // Set the starting point (top left corner of the pointer)
        mPointer.moveTo(pointerHorizontalStart(), mBoxHeight);
    
        // Define the lines
    
        // First draw a line to the top right corner
        xDistance= mPointerWidth;
        yDistance= 0;
        mPointer.rLineTo(xDistance, yDistance);
    
        // Next draw a line down to point T
        xDistance= (mPointerWidth / 2) - distancePtoA;
        yDistance= mPointerHeight - distanceTtoA;
        mPointer.rLineTo(-xDistance, yDistance); //Note: Negative sign because we are moving back to the left
    
        // Next draw the arc
        // Note: The RectF used in arcTo() is defined in absolute screen coordinates
        float boundingBoxLeft = pointerHorizontalStart() + (mPointerWidth / 2) - (2 * radius);
        float boundingBoxTop = mBoxHeight - distanceD - (2 * radius);
        float boundingBoxRight = boundingBoxLeft + (2 * radius);
        float boundingBoxBottom = boundingBoxTop + (2 * radius);
    
        RectF boundingBox = new RectF(boundingBoxLeft, boundingBoxTop, boundingBoxRight, boundingBoxBottom);
    
        // Note: While the methods in the android Math class like sin() and asin() all use Radians,
        // for some reason it was decided that arcTo() in the Path class would use Degrees,
        // so we have to convert the angles
        float startAngleInDegrees = angleAlpha * (180 / Math.PI);
        float sweepAngleInDegrees = 2 * anglePhi * (180 / Math.PI);
    
        mPointer.arcTo(boundingBox, startAngleInDegrees, sweepAngleInDegrees);
    
        // Finally draw the line from the end of the arc back up to the top left corner
        // Note: The distances are the same as from the top right corner to point T,
        // just the direction is different.
        mPointer.rLineTo(-xDistance, -yDistance); // Note: Negative in front of yDistance because we are moving up
    
        // Close off the path  
        mPointer.close();
    }