danvim / Procal

2 stars 0 forks source link

Create custom button element for keypad #3

Closed danvim closed 7 years ago

danvim commented 7 years ago

Add a new UI element that contains the popup selection layout and the actual button.

Implement code to make it act like a soft keyboard popup key.

bryanchun commented 7 years ago

1. layout/calc_btn.xml

This is the skeleton of the Custom View CalcBtn.java. This instantiates per CalcBtn. Nothing special.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="100dp"
    android:layout_height="100dp">

    <TextView
        android:id="@+id/description"
        android:gravity="center"
        android:text="txt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/button"
        android:gravity="center"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

2. layout/popupview.xml

This is instantiated per CalcBtn. Only when there's shift/alpha/... shall it be summoned. And it's editing to add Buttons, explaining why it's empty but a LinearLayout at start.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/popup"
    android:orientation="vertical"
    android:layout_width="100dp"
    android:layout_height="match_parent">

</LinearLayout>

3. layout/main.xml

All ordinary calculator keys ought to be described here (1, 2, 3, ..., sin, cos, ... log, ... lparen, ... Ans, ...), because CalcBtnS are instantiated here. (This demo includes Ans, 1, lparen, ln, 4, Cubic) Define an id with the first letter capitalised. For instance: ... android:id="@+id/Ans" for Ans key

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    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="com.elementary.bryanchun.elementary.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:id="@+id/textView" />

    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="horizontal"
        android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="60dp"
        android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true">

        <com.elementary.bryanchun.elementary.CalcBtn
            android:id="@+id/Ans"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <com.elementary.bryanchun.elementary.CalcBtn
            android:id="@+id/One"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <com.elementary.bryanchun.elementary.CalcBtn
            android:id="@+id/Lparen"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="horizontal"
        android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="200dp"
        android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true">

        <com.elementary.bryanchun.elementary.CalcBtn
            android:id="@+id/Ln"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <com.elementary.bryanchun.elementary.CalcBtn
            android:id="@+id/Cubic"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <com.elementary.bryanchun.elementary.CalcBtn
            android:id="@+id/Four"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

</RelativeLayout>

4. java/CalcBtn.java

This sets up the calc_btn layout and all methods that would ease the trouble of setting up so many keys. Note usage (as in MainActivity):

// Case: Popup Buttons
((CalcBtn) findViewById(R.id.Ans))
                .ConfigBtn(this, "Ans", "Ans", new Runnable() {
                        // Input --- id = "Ans", Button (Ordinary) Text = "Ans"
                    public void run() {
                        // Ordinary Button onClick Actions goes here.
                    }})
                .ConfigPopup(this)
                .AddPopupBtn(this, "Ans", "DRG", new Runnable() {
                        // Input --- id = "Ans", Popup Text = "DRG"
                    public void run() {
                        // First Popup Button onClick Actions goes here.
                    }})
                       // Add the second and so on Popup Buttons here
                .ListenPopup(this, "Ans");
                        // This listens for onTouch.

// Case: No-popup Buttons
((CalcBtn) findViewById(R.id.Four))
                .ConfigBtn(this, "Four", "4", new Runnable() {
                    public void run() {
                        // Ordinary Button onClick Actions goes here.
                    }});

// Note that the chaining sequence is preferably:
// Popup:    .ConfigBtn().ConfigPopup().AddPopupBtn() /*for n buttons*/ .ListenPopup()
// Normal:   .ConfigBtn()

And for the class itself:

package com.elementary.bryanchun.elementary;

import android.graphics.Rect;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.Button;
import android.view.LayoutInflater;
import android.graphics.Typeface;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.List;

public class CalcBtn extends LinearLayout {

    private PopupWindow Pw;
    private View Pv;
    private List <Button> Pbs = new ArrayList <Button>();

    public CalcBtn(Context context) {
        super(context);
        init(context);
    }

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

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

    private void init(Context context) {
        inflate(getContext(), R.layout.calc_btn, this);
        //Typeface tf = Typeface.createFromAsset(getContext().getAssets(), "fonts/fontfilename.ttf");
    }

    public CalcBtn ConfigBtn(final Context context, String mainId, String BtnText, final Runnable BtnOnClick) {
        final int Id = this.getResources().getIdentifier(mainId, "id", context.getPackageName());
        final Button Btn = (Button) findViewById(Id).findViewById(R.id.button);
        Btn.setText(BtnText);
        Btn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                BtnOnClick.run();
            }
        });
        return this;
    }

    public CalcBtn ConfigPopup(final Context context) {
        final View Pv = LayoutInflater.from(context).inflate(R.layout.popupview, null);
        final PopupWindow Pw = new PopupWindow(Pv, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        this.Pw = Pw;
        this.Pv = Pv;
        return this;
    }

    public CalcBtn AddPopupBtn(final Context context, String BtnId, String PbText, final Runnable PbOnRelease){
        final int Id = this.getResources().getIdentifier(BtnId, "id", context.getPackageName());
        final Button Btn = (Button) findViewById(Id).findViewById(R.id.button);
        final LinearLayout Popup = (LinearLayout) Pv.findViewById(R.id.popup);
        Button Pb = new Button(context);
        Pb.setText(PbText);
        Pb.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        Popup.addView(Pb);
        Pb.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                PbOnRelease.run();
            }
        });
        this.Pbs.add(Pb);
        return this;
    }

    public CalcBtn ListenPopup(final Context context, String BtnId){
        final int Id = this.getResources().getIdentifier(BtnId, "id", context.getPackageName());
        final Button Btn = (Button) findViewById(Id).findViewById(R.id.button);
        final LinearLayout Popup = (LinearLayout) Pv.findViewById(R.id.popup);
        final boolean[] IfClick = {false};
        Btn.setOnTouchListener(new View.OnTouchListener() {
            public final boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction() & MotionEvent.ACTION_MASK) {
                    case MotionEvent.ACTION_DOWN:
                        IfClick[0] = true;
                        break;
                    case MotionEvent.ACTION_UP:
                        if (IfClick[0]){
                            // ONCLICK
                            Btn.performClick();     // Summoned onClick()
                        }else {
                            Pw.dismiss();
                            Rect Pr = new Rect();
                            for (Button Pb: Pbs) {
                                Pb.getHitRect(Pr);
                                if (Pr.contains((int) event.getX(), (int) event.getY() + Pb.getHeight() * Pbs.size())) {
                                    Pb.performClick();
                                }

                            }
                        }
                        break;
                    case MotionEvent.ACTION_POINTER_DOWN:
                        break;
                    case MotionEvent.ACTION_POINTER_UP:
                        break;
                    case MotionEvent.ACTION_MOVE:
                        // ONHOLD
                        Pv.setVisibility(View.VISIBLE);
                        Pv.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
                        Pw.showAsDropDown(Btn,
                                (Btn.getWidth() - (int) Btn.getX())/2 - (Pv.getMeasuredWidth() - (int) Pv.getX())/2,
                                - Btn.getHeight() - Pv.getMeasuredHeight());
                        //Pw.showAtLocation(Btn, Gravity.TOP, (int) Pb.getWidth(), (int) Pb.getHeight());

                        Pw.setFocusable(true);
                        Pw.update();
                        Rect Pr = new Rect();
                        for (Button Pb: Pbs) {
                            Pb.getHitRect(Pr);
                            if (Pr.contains((int) event.getX(), (int) event.getY() + Pb.getHeight() * Pbs.size())) {
                                Pb.setBackgroundColor(getResources().getColor(R.color.colorAccent));
                            } else {
                                // Restore to Normal Color
                                Pb.setBackgroundColor(getResources().getColor(R.color.colorPrimaryDark));
                            }
                        }
                        IfClick[0] = false;
                        break;

                }
                return false;
            }
        });
        return this;
    }
}

Two things to consider changing are showAsDropDown, and horizontal popup buttons

5. java/MainActivity.java

This simply again shows the implementation of the CalcBtn custom view.

package com.elementary.bryanchun.elementary;

import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends Activity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        /*
        * MainActivity: setBtnName, Listener, Behaviour
        * */

        ((CalcBtn) findViewById(R.id.Ans))
                .ConfigBtn(this, "Ans", "Ans", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "onClick is performed", Toast.LENGTH_SHORT).show();
                    }})
                .ConfigPopup(this)
                .AddPopupBtn(this, "Ans", "DRG", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "Pb candidate is clicked!!!", Toast.LENGTH_SHORT).show();
                    }})
                .ListenPopup(this, "Ans");

        ((CalcBtn) findViewById(R.id.One))
                .ConfigBtn(this, "One", "1", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "onClick is performed", Toast.LENGTH_SHORT).show();
                    }})
                .ConfigPopup(this)
                .AddPopupBtn(this, "One", "S-SUM", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "Pb candidate is clicked!!!", Toast.LENGTH_SHORT).show();
                    }})
                .ListenPopup(this, "One");

        ((CalcBtn) findViewById(R.id.Lparen))
                .ConfigBtn(this, "Lparen", "(", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "onClick is performed", Toast.LENGTH_SHORT).show();
                    }})
                .ConfigPopup(this)
                .AddPopupBtn(this, "Lparen", "%", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "Pb candidate is clicked!!!", Toast.LENGTH_SHORT).show();
                    }})
                .ListenPopup(this, "Lparen");

        ((CalcBtn) findViewById(R.id.Ln))
                .ConfigBtn(this, "Ln", "ln", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "onClick is performed", Toast.LENGTH_SHORT).show();
                    }})
                .ConfigPopup(this)
                .AddPopupBtn(this, "Ln", "e^x", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "Pb candidate is clicked!!!", Toast.LENGTH_SHORT).show();
                    }})
                .AddPopupBtn(this, "Ln", "OCT", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "Pb candidate is clicked!!!", Toast.LENGTH_SHORT).show();
                    }})
                .AddPopupBtn(this, "Ln", "exp", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "Pb candidate is clicked!!!", Toast.LENGTH_SHORT).show();
                    }})
                .ListenPopup(this, "Ln");

        ((CalcBtn) findViewById(R.id.Four))
                .ConfigBtn(this, "Four", "4", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "onClick is performed", Toast.LENGTH_SHORT).show();
                    }});

        ((CalcBtn) findViewById(R.id.Cubic))
                .ConfigBtn(this, "Cubic", "^3", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "onClick is performed", Toast.LENGTH_SHORT).show();
                    }})
                .ConfigPopup(this)
                .AddPopupBtn(this, "Cubic", "^(1/3)", new Runnable() {
                    public void run() {
                        Toast.makeText(MainActivity.this, "Pb candidate is clicked!!!", Toast.LENGTH_SHORT).show();
                    }})
                .ListenPopup(this, "Cubic");
        // this, already a c_ontext

    }

}
// TODO Add styles
// TODO Specify Highlighting style
// TODO Integrate with json
bryanchun commented 7 years ago

UPDATE:

1. fixing variable (and field) naming ambiguity and specificity

2. Access Rect after declaration.

Store popupbutton instances into the ArrayList popupbtns, and store popupbuttonrects instances into another ArrayList popupbtnrects. As both starts from no elements, I expect every index of popupbtns is the same as the index of the corresponding popupbtnrects, hence pairing them up to call popupbtnrect in the for(Button popupbtn: popupbtns){ ... } loop, without the need to instantiate 2 rects per onTouch event per popupbtn.

ELUCIDATION:

An ordinary calculator key can contain one popup, this popup can contain many popupbuttons, but each popupbuttons have to come with a popuprect, in order to check whether the cursor/finger is lying inside the View or not, hence the best practice is instantiating the popuprect as we instantiate individual popupbutton in the AddPopupBtn(...) method. However issue is observed before, if (popupbtnrect.contains((int) event.getX(), (int) event.getY() + popupbtn.getHeight() * popupbtns.size())) condition was made always false, making the popupbutton detection failing, and I have known that for some reason, new Rect() was defined to have no dimension (0 times 0 size) This explains why I sticks with using new Rect() every time a popupbutton listens for touches; But then too many Rect() will be generated as a result.

package dcheungaa.procal;

import android.graphics.Rect;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.Button;
import android.view.LayoutInflater;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.List;

public class CalcBtn extends LinearLayout {

    private Button btn;
    private PopupWindow popupwindow;
    private View popupview;
    private List <Button> popupbtns = new ArrayList <>();
    private List <Rect> popupbtnrects = new ArrayList<>();

    private CalcBtn CBtn = new CalcBtn(getContext());

    public CalcBtn(Context context) {
        super(context);
        init(context);
    }

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

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

    private void init(Context context) {
        inflate(getContext(), R.layout.calc_btn, this);
        //Typeface tf = Typeface.createFromAsset(getContext().getAssets(), "fonts/fontfilename.ttf");
    }

    public CalcBtn ConfigBtn(final Context context, String StyleAttr, String BtnText, String key) {
        btn  = new Button(context, null, getResources().getIdentifier(
                StyleAttr,
                "attr",
                context.getPackageName()
                )
        );
        btn.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        btn.setText(BtnText);
        btn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                // TODO Call Main_Add_Stack(key);
            }
        });
        return this;
    }

    public CalcBtn ConfigPopup(final Context context) {
        popupview = LayoutInflater.from(context).inflate(R.layout.popup_view, null);
        popupwindow = new PopupWindow(popupview, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);;
        return this;
    }

    public CalcBtn AddPopupBtn(final Context context, String PbText, String Pbkey){
        final LinearLayout Popup = (LinearLayout) popupview.findViewById(R.id.popup);
        Button popupbtn = new Button(context);
        popupbtn.setText(PbText);
        popupbtn.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        Popup.addView(popupbtn);
        popupbtn.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                // TODO Call Main_Add_Stack(Pbkey);
            }
        });
        popupbtns.add(popupbtn);
        Rect popupbtnrect = new Rect();
        popupbtnrects.add(popupbtnrect);
        return this;
    }

    public CalcBtn ListenPopup(final Context context){
        final boolean[] IfClick = {false};
        btn.setOnTouchListener(new View.OnTouchListener() {
            public final boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction() & MotionEvent.ACTION_MASK) {
                    case MotionEvent.ACTION_DOWN:
                        IfClick[0] = true;
                        break;
                    case MotionEvent.ACTION_UP:
                        if (IfClick[0]){
                            // ONCLICK
                            btn.performClick();
                        }else {
                            popupwindow.dismiss();
                            for (Button popupbtn: popupbtns) {
                                Rect popupbtnrect = popupbtnrects.get(popupbtns.indexOf(popupbtn));
                                popupbtn.getHitRect(popupbtnrect);
                                if (popupbtnrect.contains((int) event.getX(), (int) event.getY() + popupbtn.getHeight() * popupbtns.size())) {
                                    popupbtn.performClick();
                                }

                            }
                        }
                        break;
                    case MotionEvent.ACTION_POINTER_DOWN:
                        break;
                    case MotionEvent.ACTION_POINTER_UP:
                        break;
                    case MotionEvent.ACTION_MOVE:
                        // ONHOLD
                        popupview.setVisibility(View.VISIBLE);
                        popupview.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
                        popupwindow.showAsDropDown(btn,
                                (btn.getWidth() - (int) btn.getX())/2 - (popupview.getMeasuredWidth() - (int) popupview.getX())/2,
                                - btn.getHeight() - popupview.getMeasuredHeight());
                        //Pw.showAtLocation(Btn, Gravity.TOP, (int) Pb.getWidth(), (int) Pb.getHeight());

                        popupwindow.setFocusable(true);
                        popupwindow.update();
                        for (Button popupbtn: popupbtns) {
                            Rect popupbtnrect = popupbtnrects.get(popupbtns.indexOf(popupbtn));
                            popupbtn.getHitRect(popupbtnrect);
                            if (popupbtnrect.contains((int) event.getX(), (int) event.getY() + popupbtn.getHeight() * popupbtns.size())) {
                                popupbtn.setBackgroundColor(getResources().getColor(R.color.colorAccent));
                            } else {
                                // Restore to Normal Color
                                popupbtn.setBackgroundColor(getResources().getColor(R.color.colorPrimaryDark));
                            }
                        }
                        IfClick[0] = false;
                        break;

                }
                return false;
            }
        });
        return this;
    }
}