Mick Mick - 1 month ago 14
Android Question

Setting width of SeekBar to make "swipe to unlock" effect

I am attempting to make a swipe to unlock feature using a SeekBar. The look I am aiming for is shown here:

enter image description here

This is composed of two images, a background, and a button. I put both the background and the SeekBar in a FrameLayout so that the SeekBar should sit on top of the background.

Like so:

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" >

<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="Testing 123..." />

<FrameLayout
android:layout_height="wrap_content"
android:layout_width="wrap_content" >

<ImageView
android:id="@+id/ImageView01"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="center"
android:src="@drawable/unlockback" />

<SeekBar
android:id="@+id/myseek"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:max="100"
android:progressDrawable="@android:color/transparent"
android:thumb="@drawable/unlockbut" />

</FrameLayout>

</LinearLayout>


Unfortunately the end result looks like this (in eclipse):

enter image description here

I seem to be unable to make the SeekBar match the size of the FrameLayout. You can see the size of the Seekbar represented by a thin blue frame in the image above. The frame has two small solid blue squares which you can grab with the mouse pointer for resizing. But if I use my mouse pointer to drag the little blue square to match the full width of the FrameView, then as soon as I let go of the mouse, the square pings back to its original (too small) size.

What can I do to fix this?.. If I can achieve swipe to unlock in a fundamentally different way, then I'm interested in that too.

Answer

As I promised I will see what I can do. I have not used your images and used android graphics to do the drawing as that makes the whole thing more customizable and and scalable. If you do insist in drawing your images, use canvas.drawBitmap... it's pretty simple. The main logic can stay the same.

I may come back and add some fancy animations and visual effects, but for now I left some commented out code to play with shaders and gradients as I am a bit short on time at the moment.

Let's get to it... First crate attrs.xml in /resources/ and add this to it.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="SlideToUnlock">
        <attr name="sliderColor" format="color"/>
        <attr name="cancelOnYExit" format="boolean"/>
        <attr name="slideToUnlockText" format="string"/>
        <attr name="slideToUnlockTextColor" format="color"/>
        <attr name="slideToUnlockBackgroundColor" format="color"/>
        <attr name="cornerRadiusX" format="dimension"/>
        <attr name="cornerRadiusY" format="dimension"/>
    </declare-styleable>
</resources>

And then SlideToUnlock.java

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.EmbossMaskFilter;
import android.graphics.MaskFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Typeface;
import android.os.Build;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by ksenchy on 29.4.2015.
 */
public class SlideToUnlock extends View {

    public interface OnSlideToUnlockEventListener {
        public void onSlideToUnlockCanceled();

        public void onSlideToUnlockDone();
    }

    private int measuredWidth, measuredHeight;
    private float density;
    private OnSlideToUnlockEventListener externalListener;
    private Paint mBackgroundPaint, mTextPaint, mSliderPaint;
    private float rx, ry; // Corner radius
    private Path mRoundedRectPath;
    private String text = "Unlock  →";

    float x;
    float event_x, event_y;
    float radius;
    float X_MIN, X_MAX;
    private boolean ignoreTouchEvents;

    // Do we cancel when the Y coordinate leaves the view?
    private boolean cancelOnYExit;
    private boolean useDefaultCornerRadiusX, useDefaultCornerRadiusY;


    /**
     * Default values *
     */
    int backgroundColor = 0xFF807B7B;
    int textColor = 0xFFFFFFFF;
    int sliderColor = 0xAA404040;


    public SlideToUnlock(Context context) {
        super(context);
        init(context, null, 0);
    }

    public SlideToUnlock(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

    public SlideToUnlock(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    public OnSlideToUnlockEventListener getExternalListener() {
        return externalListener;
    }

    public void setExternalListener(OnSlideToUnlockEventListener externalListener) {
        this.externalListener = externalListener;
    }

    private void init(Context context, AttributeSet attrs, int style) {

        Resources res = getResources();
        density = res.getDisplayMetrics().density;

        TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SlideToUnlock, style, 0);

        String tmp = a.getString(R.styleable.SlideToUnlock_slideToUnlockText);
        text = TextUtils.isEmpty(tmp) ? text : tmp;
        rx = a.getDimension(R.styleable.SlideToUnlock_cornerRadiusX, rx);
        useDefaultCornerRadiusX = rx == 0;
        ry = a.getDimension(R.styleable.SlideToUnlock_cornerRadiusX, ry);
        useDefaultCornerRadiusY = ry == 0;
        backgroundColor = a.getColor(R.styleable.SlideToUnlock_slideToUnlockBackgroundColor, backgroundColor);
        textColor = a.getColor(R.styleable.SlideToUnlock_slideToUnlockTextColor, textColor);
        sliderColor = a.getColor(R.styleable.SlideToUnlock_sliderColor, sliderColor);
        cancelOnYExit = a.getBoolean(R.styleable.SlideToUnlock_cancelOnYExit, false);

        a.recycle();

        mRoundedRectPath = new Path();

        mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mBackgroundPaint.setStyle(Paint.Style.FILL);
        mBackgroundPaint.setColor(backgroundColor);

        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setColor(textColor);
        mTextPaint.setTypeface(Typeface.create("Roboto-Thin", Typeface.NORMAL));

        mSliderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mSliderPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mSliderPaint.setColor(sliderColor);
        mSliderPaint.setStrokeWidth(2 * density);

        if (!isInEditMode()) {
            // Edit mode does not support shadow layers
            // mSliderPaint.setShadowLayer(10.0f, 0.0f, 2.0f, 0xFF000000);
            //mSliderPaint.setMaskFilter(new EmbossMaskFilter(new float[]{1, 1, 1}, 0.4f, 10, 8.2f));
            float[] direction = new float[]{0.0f, -1.0f, 0.5f};
            MaskFilter filter = new EmbossMaskFilter(direction, 0.8f, 15f, 1f);
            mSliderPaint.setMaskFilter(filter);
            //mSliderPaint.setShader(new LinearGradient(8f, 80f, 30f, 20f, Color.RED,Color.WHITE, Shader.TileMode.MIRROR));
            //mSliderPaint.setShader(new RadialGradient(8f, 80f, 90f, Color.RED,Color.WHITE, Shader.TileMode.MIRROR));
            //mSliderPaint.setShader(new SweepGradient(80, 80, Color.RED, Color.WHITE));
            //mSliderPaint.setMaskFilter(new BlurMaskFilter(15, BlurMaskFilter.Blur.OUTER));
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measuredHeight = getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec);
        measuredWidth = getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec);

        if (useDefaultCornerRadiusX) {
            rx = measuredHeight * 0.52f;
        }
        if (useDefaultCornerRadiusY) {
            ry = measuredHeight * 0.52f;
        }
        mTextPaint.setTextSize(measuredHeight / 3.0f);

        radius = measuredHeight * 0.45f;
        X_MIN = 1.2f * radius;
        X_MAX = measuredWidth - X_MIN;
        x = X_MIN;

        setMeasuredDimension(measuredWidth, measuredHeight);
    }

    private void drawRoundRect(Canvas c) {
        mRoundedRectPath.reset();
        mRoundedRectPath.moveTo(rx, 0);
        mRoundedRectPath.lineTo(measuredWidth - rx, 0);
        mRoundedRectPath.quadTo(measuredWidth, 0, measuredWidth, ry);
        mRoundedRectPath.lineTo(measuredWidth, measuredHeight - ry);
        mRoundedRectPath.quadTo(measuredWidth, measuredHeight, measuredWidth - rx, measuredHeight);
        mRoundedRectPath.lineTo(rx, measuredHeight);
        mRoundedRectPath.quadTo(0, measuredHeight, 0, measuredHeight - ry);
        mRoundedRectPath.lineTo(0, ry);
        mRoundedRectPath.quadTo(0, 0, rx, 0);
        c.drawPath(mRoundedRectPath, mBackgroundPaint);
    }

    @SuppressLint("NewApi")
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (measuredHeight <= 0 || measuredWidth <= 0) {
            // There is not much we can draw :/
            return;
        }

        if (Build.VERSION.SDK_INT >= 21) {
            canvas.drawRoundRect(0, 0, measuredWidth, measuredHeight, rx, ry, mBackgroundPaint);
        }
        else {
            drawRoundRect(canvas);
        }


        // Draw the text in center
        float xPos = ((measuredWidth - mTextPaint.measureText(text)) / 2.0f);
        float yPos = (measuredHeight / 2.0f);
        float titleHeight = Math.abs(mTextPaint.descent() + mTextPaint.ascent());
        yPos += titleHeight / 2.0f;
        canvas.drawText(text, xPos, yPos, mTextPaint);


        canvas.drawCircle(x, measuredHeight * 0.5f, radius, mSliderPaint);

    }

    private void onCancel() {
        reset();
        if (externalListener != null) {
            externalListener.onSlideToUnlockCanceled();
        }
    }

    private void onUnlock() {
        if (externalListener != null) {
            externalListener.onSlideToUnlockDone();
        }
    }

    private void reset() {
        x = X_MIN;
        invalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                ignoreTouchEvents = false;
                reset();
                return true;
            case MotionEvent.ACTION_DOWN:
                // Is within the circle??
                event_x = event.getX(0);
                event_y = event.getY(0);
                double squareRadius = radius * radius;
                double squaredXDistance = (event_x - X_MIN) * (event_x - X_MIN);
                double squaredYDistance = (event_y - measuredHeight / 2) * (event_y - measuredHeight / 2);

                if (squaredXDistance + squaredYDistance > squareRadius) {
                    // User touched outside the button, ignore his touch
                    ignoreTouchEvents = true;
                }

                return true;
            case MotionEvent.ACTION_CANCEL:
                ignoreTouchEvents = true;
                onCancel();
            case MotionEvent.ACTION_MOVE:
                if (!ignoreTouchEvents) {
                    event_x = event.getX(0);
                    if (cancelOnYExit) {
                        event_y = event.getY(0);
                        if (event_y < 0 || event_y > measuredHeight) {
                            ignoreTouchEvents = true;
                            onCancel();
                        }
                    }

                    x = event_x > X_MAX ? X_MAX : event_x < X_MIN ? X_MIN : event_x;
                    if (event_x >= X_MAX) {
                        ignoreTouchEvents = true;
                        onUnlock();
                    }
                    invalidate();
                }
                return true;
            default:
                return super.onTouchEvent(event);
        }
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#FF000000">

    <your.package.SlideToUnlock
        android:id="@+id/slideToUnlock"
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:layout_centerInParent="true"/>

    <your.package.SlideToUnlock
        android:id="@+id/slideToUnlock2"
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:layout_below="@+id/slideToUnlock"
        android:layout_centerInParent="true"
        android:layout_marginTop="50dp"
        app:cancelOnYExit="true"
        app:slideToUnlockBackgroundColor="#808080"
        app:slideToUnlockText="Slide to unlock"
        app:slideToUnlockTextColor="#03A9F4"
        app:sliderColor="#AAFFE97F"/>

</RelativeLayout>

MainActivity.java

import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.widget.Toast;


public class MainActivity extends ActionBarActivity implements SlideToUnlock.OnSlideToUnlockEventListener {

    private SlideToUnlock slideToUnlockView, slideToUnlockView2;
    private Toast toast;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        slideToUnlockView = (SlideToUnlock) findViewById(R.id.slideToUnlock);
        slideToUnlockView.setExternalListener(this);

        slideToUnlockView2 = (SlideToUnlock) findViewById(R.id.slideToUnlock2);
        slideToUnlockView2.setExternalListener(this);
    }

    private void showToast(String text) {
        if (toast != null) {
            toast.cancel();
        }

        toast = Toast.makeText(this, text, Toast.LENGTH_SHORT);
        toast.show();
    }

    @Override
    public void onSlideToUnlockCanceled() {
        showToast("Canceled");
    }

    @Override
    public void onSlideToUnlockDone() {
        showToast("Unlocked");
    }
}

You can download the whole project here. Enjoy :)

This is the final result.

Slide to unlock