Wednesday, May 27, 2015

Indeterminate progressbar with rounded corners

The build in android progressbar can be customised to use different drawables, but than the corners would be sharp. Our designer need it to be rounded. My solution is flexible enough that it can be cropped to any shape.



This is how I did it (essentially creating a custom Drawable from program code).

It gets a Drawable, tiles it (repeating it) and crops it to a shape that was provided by the Callable. It handles AnimationDrawable also: creates a similar AnimationDrawable tiling and cropping each frame. For the clip shape I had to use Callable - which is essentially a factory - because a new ShapeDrawable has to be created for each animation frame.  The inner class EquallyRoundCornerRect can be used for the most common scenario: equal round corner.

/**
 * It gets a {@link android.graphics.drawable.Drawable}, tiles it (repeating it) and crops it to a shape that was provided by the
 * {@link java.util.concurrent.Callable}. It handles {@link android.graphics.drawable.AnimationDrawable} also: creates a similar
 * {@link android.graphics.drawable.AnimationDrawable} tiling and cropping each frame. For the clip shape I had to use
 * {@link java.util.concurrent.Callable} - which is essentially a factory - because a new {@link android.graphics.drawable.ShapeDrawable}
 * has to be created for each animation frame.
 * The inner class {@link EquallyRoundCornerRect} can be used for the most common scenario: equal round corner.
 */
public class TiledClippedDrawable {

    public static Drawable createFrom(final Drawable originalDrawable, final Callable<ShapeDrawable> clipShape) {
        return tileifyIndeterminate(originalDrawable, clipShape);
    }

    private static Drawable tileify(Drawable drawable, final Callable<ShapeDrawable> clipShape) {
        final Bitmap tileBitmap = ((BitmapDrawable) drawable).getBitmap();
        try {
            final ShapeDrawable shapeDrawable = clipShape.call();

            final BitmapShader bitmapShader = new BitmapShader(tileBitmap,
                    Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
            shapeDrawable.getPaint().setShader(bitmapShader);

            return new ClipDrawable(shapeDrawable, Gravity.LEFT, ClipDrawable.HORIZONTAL);
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Drawable tileifyIndeterminate(Drawable drawable, final Callable<ShapeDrawable> clipShape) {
        if (drawable instanceof AnimationDrawable) {
            AnimationDrawable background = (AnimationDrawable) drawable;
            final int N = background.getNumberOfFrames();
            AnimationDrawable newBg = new AnimationDrawable();
            newBg.setOneShot(background.isOneShot());

            for (int i = 0; i < N; i++) {
                Drawable frame = tileify(background.getFrame(i), clipShape);
                frame.setLevel(10000);
                newBg.addFrame(frame, background.getDuration(i));
            }
            newBg.setLevel(10000);
            drawable = newBg;
            return drawable;
        }
        return tileify(drawable, clipShape);
    }

    public static class EquallyRoundCornerRect implements Callable<ShapeDrawable> {
        private final int cornerRadiusPixel;

        public EquallyRoundCornerRect(final int cornerRadiusPixel) {
            this.cornerRadiusPixel = cornerRadiusPixel;
        }

        @Override
        public ShapeDrawable call() throws Exception {
            final float[] roundedCorners = new float[] {cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel,
                    cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel, cornerRadiusPixel};
            return new ShapeDrawable(new RoundRectShape(roundedCorners, null, null));
        }
    }
}

And this is how it can be used:

final int cornerRadius = getResources().getDimensionPixelOffset(R.dimen.progressbar_corner_radius);
final Drawable originalDrawable = getResources().getDrawable(R.drawable.progress_bar_indeterminate_horizontal);
final Drawable roundedAnimation = TiledClippedDrawable.createFrom(originalDrawable, new TiledClippedDrawable.EquallyRoundCornerRect(cornerRadius));
unpackingSpinner = (ProgressBar) backView.findViewById(R.id.gallery_item_unpacking_spinner);
unpackingSpinner.setIndeterminateDrawable(roundedAnimation);


Here the R.dimen.progressbar_corner_radius is a number, and the R.drawable.progress_bar_indeterminate_horizontal is an animationlist. Each of those install_progressbar_indeterminate1 images contain only a fragment, because they will be tiled anyway.
<animation-list
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:oneshot="false">
    <item android:drawable="@drawable/install_progressbar_indeterminate1" android:duration="200" />
    <item android:drawable="@drawable/install_progressbar_indeterminate2" android:duration="200" />
    <item android:drawable="@drawable/install_progressbar_indeterminate3" android:duration="200" />
</animation-list>

Maven stuff

First off, here is a page that contains some global maven properties. Some best practices.

  • Deploy lib to local maven repo
    mvn install:install-file -Dfile=target/eyjafjalla.apklib -DgroupId=com.example -DartifactId=eyjafjalla -Dversion=1 -Dpackaging=apklib
    

  • Running maven with profiles:
    mvn groupId:artifactId:goal -P profile-1,profile-2
    

  • Deactivating profiles:
    mvn groupId:artifactId:goal -P !profile-1,!profile-2
    
    This can be used to deactivate profiles marked as activeByDefault or profiles that would otherwise be activated through their activation config.

  • Available properties:
    mvn help:system
    

  • By default run all tests, but this can be disabled:
    <profile>
        <id>by_default_run_all_tests</id>
        <activation>
            <property>
                <name>!notest</name>
            </property>
        </activation>
        <properties>
            <notest>false</notest>
        </properties>
    </profile>
    ...
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.6</version>
        <configuration>
            <skip>${notest}</skip>
            <skipTests>false</skipTests>
        </configuration>
    </plugin>
    
    Run with
    mvn install -Dnotest
    Related stackoverflow question

  • Or more generic default overridable parameter:
    <profile>
        <id>by_default_copy_poker_assets</id>
        <activation>
            <property>
                <name>!noassetoverwrite</name>
            </property>
        </activation>
        <properties>
            <overwrite_poker_asset>true</overwrite_poker_asset>
        </properties>
    </profile>
    
    <profile>
        <id>do_not_copy_poker_assets</id>
        <activation>
            <property>
                <name>noassetoverwrite</name>
            </property>
        </activation>
        <properties>
            <overwrite_poker_asset>false</overwrite_poker_asset>
        </properties>
    </profile>
    

  • Execute shell command:
    <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>1.1.1</version>
        <executions>
            <execution>
                <id>some-execution</id>
                <phase>validate</phase>
                <goals>
                    <goal>exec</goal>
                </goals>
            </execution>
        </executions>
        <configuration>
            <executable>python</executable>
            <workingDirectory>${project.basedir}/..</workingDirectory>
            <arguments>
                <argument>~/Rockaway/file_date_check/file_date_check.py</argument>
            </arguments>
        </configuration>
    </plugin>
    

  • Plugin for IntelliJ to permute a list of enything

    Many times I encounter the situation, where I want to reorder a list: enum values, DataSet test cases, parameters of a function, elements of a list, etc. Cut and paste is just too much effort, and extra care has to be taken to keep formatting. So I've written a small code snippet to rearrange these lists. I use IntelliJ's LivePlugin (on github) to use it quickly (but it could be converted to a full Java plugin). But unfortunatelly it handles Groovy script. Testing groovy script I find it pain in the ass, so I've created a Java barebone with test, and than converted it to Groovyscript (I myself find this approach a bit stupid, though).

    How it works? If your carret is sitting in between any type of brackets, it finds the innermost it sits in. It finds the list element (text, that can contain spaces, other closed brackets, comas inside those brackets). With Ctrl+Alt+L it swaps it to the right, with Ctrl+Alt+K, it swaps it to the left. The carret moves with the item.

    Known limitations. If there is an unclosed backet (syntactically incorret code), it doesn't handle it well. If the carret is not inside a backet, the behaviour is unpredicted.

    The rest of the post is just source code. The Java fragment:

    int start;
        int end;
        int mid;
        int midLeft;
        int midRight;
        public String doTheFlop(final String input, final int carretPos) {
            start = carretPos - 1;
            mid = carretPos;
    
            travelToBeginning(input);
    
            travelToMid(input);
    
            if (isRightLimiter(input.charAt(mid))) {
                return null;
            }
    
            midLeft = mid ;
            midRight = mid ;
            travelToMidLeft(input);
    
            travelToMidRight(input);
    
            end = midRight;
            travelToEnd(input);
    
            return input.substring(midRight, end) + input.substring(midLeft, midRight) + input.substring(start, midLeft);
        }
    
        private void travelToBeginning(final String input) {
            int groupDepth = 0;
            while (!((input.charAt(start) == ',' && groupDepth == 0)
                   || (isLeftLimiter(input.charAt(start)) && groupDepth == 0))) {
                if (isRightLimiter(input.charAt(start))) {
                    groupDepth++;
                }
                if (isLeftLimiter(input.charAt(start))) {
                    groupDepth--;
                }
                start--;
            }
            start++;
            while (Character.isWhitespace(input.charAt(start))) {
                start++;
            }
        }
    
        private void travelToMid(final String input) {
            int groupDepth = 0;
            while (!((input.charAt(mid) == ',' && groupDepth == 0)
                     || (isRightLimiter(input.charAt(mid)) && groupDepth == 0))) {
                if (isLeftLimiter(input.charAt(mid))) {
                    groupDepth++;
                }
                if (isRightLimiter(input.charAt(mid))) {
                    groupDepth--;
                }
                mid++;
            }
        }
    
        private void travelToMidLeft(final String input) {
            // this currently points to a coma or bracket
            midLeft--;
            while (Character.isWhitespace(input.charAt(midLeft))) {
                midLeft--;
            }
            midLeft++;
        }
    
        private void travelToMidRight(final String input) {
            // this currently points to a coma or bracket
            midRight++;
            while (Character.isWhitespace(input.charAt(midRight))) {
                midRight++;
            }
        }
    
        private void travelToEnd(final String input) {
            int groupDepth = 0;
            while (!((input.charAt(end) == ',' && groupDepth == 0)
                     || (isRightLimiter(input.charAt(end)) && groupDepth == 0))) {
                if (isLeftLimiter(input.charAt(end))) {
                    groupDepth++;
                }
                if (isRightLimiter(input.charAt(end))) {
                    groupDepth--;
                }
                end++;
            }
    
            end--;
            while (Character.isWhitespace(input.charAt(end))) {
                end--;
            }
            end++;
        }
    
        private boolean isLeftLimiter(final char c) {
            return c == '(' || c == '<' || c == '[' || c == '{';
        }
    
        private boolean isRightLimiter(final char c) {
            return c == ')' || c == '>' || c == ']' || c == '}';
        }
    

    Test for this:

    public static class Stuff extends SimpleTestVectors {
            @Override
            protected Object[][] generateTestVectors() {
                return new Object[][] {
                        //"(  final Broad broadcast,  final  Frame   frame,final   Logger logger)"
                        //(Logger logger,    Frame< String, Integer  > frame, final   @Named ( "quickDepositStatusIntentTranslator" ) Broad broadcast)
                        {"(  ", "final Broad broadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad br", "oadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad br", "oadcast,  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad broadcast", ",  final  Frame   frame,final   Logger logger)", "final  Frame   frame,  final Broad broadcast"},  //, "(  final  Frame   frame,  final Broad broadcast,final   Logger logger)"
                        {"(  final Broad broadcast,", "  final  Frame   frame,final   Logger logger)", "final   Logger logger,final  Frame   frame"},  //, "(  final Broad broadcast,  final   Logger logger,final  Frame   frame)"
                        {"(  final Broad broadcast,  final  Fr", "ame   frame,final   Logger logger)", "final   Logger logger,final  Frame   frame"},  //, "(  final Broad broadcast,  final   Logger logger,final  Frame   frame)"
                        {"(  final Broad broadcast,  final  Frame   frame,final   ", "Logger logger)", null},  //, "(  final Broad broadcast,  final  Frame   frame,final   Logger logger)"
    
                        {"(Broad broadcast, Fr", "ame frame      , Logger logger   )", "Logger logger      , Frame frame"},  //, "(final Broad broadcast, final Logger logger      , final Frame frame   )"
    
                        {"(final   @", "Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", "Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast"}, //"(Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger)"},
                        {"(final   @Named ( \"quickDepositS", "tatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", null}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ", ") Broad broadcast, Frame< String, Integer  > frame, Logger logger)", null}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad", " broadcast, Frame< String, Integer  > frame, Logger logger)", "Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast"}, //"(Frame< String, Integer  > frame, final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, ", "Frame< String, Integer  > frame, Logger logger)", "Logger logger, Frame< String, Integer  > frame"}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Logger logger, Frame< String, Integer  > frame)"},
                        {"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< S", "tring, Integer  > frame, Logger logger)", "Integer, String"}, //"(final   @Named ( \"quickDepositStatusIntentTranslator\" ) Broad broadcast, Frame< Integer, String  > frame, Logger logger)"},
    
                        {"(final   @Named ( \"", " foo () \" , \" bar () \" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)", "\" bar () \" , \" foo () \""}, //"(final   @Named( \" bar () \" , \" foo () \" ) Broad broadcast, Frame< String, Integer  > frame, Logger logger)""},
                };
            }
        }
    
        @Test
        @DataSet(testData = Stuff.class)
        public void dummy_FIXME() throws InvalidDataSetException {
            // init
    
            // run
            final String result = sut.doTheFlop(rule.getString(0) + rule.getString(1), rule.getString(0).length());
    
            // verify
            assertThat(result, equalTo(rule.getString(2)));
        }
    
    

    And the final Groovyscript refined:

    import com.intellij.openapi.actionSystem.AnActionEvent
    import com.intellij.openapi.application.ApplicationManager
    import com.intellij.openapi.editor.ScrollType
    import com.intellij.openapi.editor.SelectionModel
    import com.intellij.openapi.util.TextRange
    
    import static liveplugin.PluginUtil.*
    import static liveplugin.PluginUtil.showInConsole
    import static liveplugin.PluginUtil.showInConsole
    import static liveplugin.PluginUtil.showInConsole
    
    
    registerAction("swap to right", "alt ctrl L") { AnActionEvent event ->
        def editor = currentEditorIn(event.project)
        def input = editor.getDocument().getChars()
        def caretModel = editor.getCaretModel()
        def selectionModel = editor.getSelectionModel()
    
    
        int start = caretModel.offset - 1;
        if (start < 0) return;
        int mid = start + 1;
        int end;
        int midLeft;
        int midRight;
    
        //travelToBeginning
        int groupDepth = 0;
        while (!((input[start] == ',' && groupDepth == 0)
                || (isLeftLimiter(input[start]) && groupDepth == 0))) {
            if (isRightLimiter(input[start])) {
                groupDepth++;
            }
            if (isLeftLimiter(input[start])) {
                groupDepth--;
            }
            start--;
        }
        // fallback to first non-whitespace char
        start++;
        while (isWhitespace(input[start])) {
            start++;
        }
    
        //travelToMid
        groupDepth = 0;
        while (!((input[mid] == ',' && groupDepth == 0)
                || (isRightLimiter(input[mid]) && groupDepth == 0))) {
            if (isLeftLimiter(input[mid])) {
                groupDepth++;
            }
            if (isRightLimiter(input[mid])) {
                groupDepth--;
            }
            mid++;
        }
    
        // this is already the last element
        if (isRightLimiter(input[mid])) {
            return;
        }
    
        midLeft = mid;
        midRight = mid;
    
        //travelToMidLeft
        // this currently points to a coma or bracket
        midLeft--;
        while (isWhitespace(input[midLeft])) {
            midLeft--;
        }
        midLeft++;
    
        //travelToMidRight
        // this currently points to a coma or bracket
        midRight++;
        while (isWhitespace(input[midRight])) {
            midRight++;
        }
    
        //travelToEnd
        end = midRight;
        groupDepth = 0;
        while (!((input[end] == ',' && groupDepth == 0)
                || (isRightLimiter(input[end]) && groupDepth == 0))) {
            if (isLeftLimiter(input[end])) {
                groupDepth++;
            }
            if (isRightLimiter(input[end])) {
                groupDepth--;
            }
            end++;
        }
    
        // fallback to last non-whitespace char
        end--;
        while (isWhitespace(input[end])) {
            end--;
        }
        end++;
    
    
        // do the swap
        String text = editor.document.getText(new TextRange(start, end))
        String swappedText = text.substring(midRight - start, end - start) + text.substring(midLeft - start, midRight - start) + text.substring(0, midLeft - start)
    
        runDocumentWriteAction(event.project, editor.document, "Swap to right a list element", "Permute plugin (mnw)") {
            editor.document.replaceString(start, end, swappedText)
        }
    
        caretModel.moveToOffset(end)
        editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
    }
    
    registerAction("swap to left", "alt ctrl K") { AnActionEvent event ->
        def editor = currentEditorIn(event.project)
        def input = editor.getDocument().getChars()
        def caretModel = editor.getCaretModel()
        def selectionModel = editor.getSelectionModel()
    
    
        int start = caretModel.offset - 1;
        if (start < 0) return;
        int carret = start;
        int lastComa;
        int mid;
        int end;
        int midLeft;
        int midRight;
    
        //travelToBeginning
        int groupDepth = 0;
        while (!(isLeftLimiter(input[start]) && groupDepth == 0)) {
            if (isRightLimiter(input[start])) {
                groupDepth++;
            }
            if (isLeftLimiter(input[start])) {
                groupDepth--;
            }
            start--;
        }
        lastComa = start;
    
        //travel backwards ToMid from start keeping track if we find coma (only the last is important)
        groupDepth = 0;
        mid = start + 1;
        while (mid <= carret) {
            if (isLeftLimiter(input[mid])) {
                groupDepth++;
            }
            if (isRightLimiter(input[mid])) {
                groupDepth--;
            }
            if (input[mid] == ',' && groupDepth == 0) {
                start = lastComa;
                lastComa = mid;
            }
            mid++;
        }
    
        // this is already the first element
        if (lastComa == start) {
            return;
        }
    
        // skip whitespaces from start
        start++;
        while (isWhitespace(input[start])) {
            start++;
        }
    
        midLeft = lastComa;
        midRight = lastComa;
    
        // skipp whitespaces around the coma
        // this currently points to a coma or bracket
        midLeft--;
        while (isWhitespace(input[midLeft])) {
            midLeft--;
        }
        midLeft++;
        // this currently points to a coma or bracket
        midRight++;
        while (isWhitespace(input[midRight])) {
            midRight++;
        }
    
        //travelToEnd
        end = midRight;
        groupDepth = 0;
        while (!((input[end] == ',' && groupDepth == 0)
                || (isRightLimiter(input[end]) && groupDepth == 0))) {
            if (isLeftLimiter(input[end])) {
                groupDepth++;
            }
            if (isRightLimiter(input[end])) {
                groupDepth--;
            }
            end++;
        }
    
        // skip ending whitespaces from end
        end--;
        while (isWhitespace(input[end])) {
            end--;
        }
        end++;
    
    
        // do the swap
        String text = editor.document.getText(new TextRange(start, end))
        String swappedText = text.substring(midRight - start, end - start) + text.substring(midLeft - start, midRight - start) + text.substring(0, midLeft - start)
    
        runDocumentWriteAction(event.project, editor.document, "Swap to left a list element", "Permute plugin (mnw)") {
            editor.document.replaceString(start, end, swappedText)
        }
    
    
        caretModel.moveToOffset(start + end - midRight)
        editor.scrollingModel.scrollToCaret(ScrollType.MAKE_VISIBLE)
    }
    
    private boolean isWhitespace(char charAt) {
        return charAt == " " || charAt == "\t" || charAt == "\n" || charAt == "\r" || charAt == "\f";
    }
    
    private boolean isLeftLimiter(final char c) {
        return c == '(' || c == '<' || c == '[' || c == '{';
    }
    
    private boolean isRightLimiter(final char c) {
        return c == ')' || c == '>' || c == ']' || c == '}';
    }
    
    show("permute Loaded")