Android 如何创建简单的自定义 UI 元素

Android how to create simple custom UI elements

我想在 Android 中创建简单的自定义 UI 元素,就像屏幕截图中的元素一样:

灯泡的大小应始终相同,但矩形的宽度应有所不同。这样做的一种选择是使用 Canvas 元素。但我想问一下是否还有更简单的方法来做到这一点。是否可以仅通过使用 XML 文件来执行此操作?我想在 LayoutEditor 中使用这些 UI 元素,例如我可以在 XML 布局文件中或以编程方式调整宽度和高度的 TextView。

知道如何以简单的方式做到这一点吗?

Update:我尝试了 Cheticamp 建议的方法,我的片段中有以下代码:

public class Test extends Fragment implements Runnable {

    /*
    Game variables
     */

    public static final int DELAY_MILLIS = 100;
    public static final int TIME_OF_A_LEVEL_IN_SECONDS = 90;
    private int currentTimeLeftInTheLevel_MILLIS;
    private Handler handler = new Handler();
    private FragmentGameBinding binding;

    private boolean viewHasBeenCreated = false;


    public Test() {
        // Required empty public constructor
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentGameBinding.inflate(inflater, container, false);
        getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        container.getContext();
        viewHasBeenCreated = true;
        startRound();
        return binding.getRoot();


    }

    public void startRound () {
        currentTimeLeftInTheLevel_MILLIS =TIME_OF_A_LEVEL_IN_SECONDS * 1000;
        updateScreen();
        handler.postDelayed(this, 1000);

    }
    private void updateScreen() {
        binding.textViewTimeLeftValue.setText("" + currentTimeLeftInTheLevel_MILLIS/1000);

        /*
        IMPORTANT PART: This should create a simple custom UI element but it creates an error
         */
        View view = new View(getActivity());
        view.setLayoutParams(new ViewGroup.LayoutParams(100, 100));
        Drawable dr = ContextCompat.getDrawable(getActivity(),R.drawable.light_bulb_layer_list);
        view.setBackground(dr);

        ConstraintLayout constraintLayout = binding.constraintLayout;
        ConstraintSet constraintSet = new ConstraintSet();
        constraintSet.clone(constraintLayout);
        constraintSet.connect(view.getId(),ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID,ConstraintSet.BOTTOM,0);
        constraintSet.connect(view.getId(),ConstraintSet.TOP,ConstraintSet.PARENT_ID ,ConstraintSet.TOP,0);
        constraintSet.connect(view.getId(),ConstraintSet.LEFT,ConstraintSet.PARENT_ID ,ConstraintSet.LEFT,0);
        constraintSet.connect(view.getId(),ConstraintSet.RIGHT,ConstraintSet.PARENT_ID ,ConstraintSet.RIGHT,0);
        constraintSet.setHorizontalBias(view.getId(), 0.16f);
        constraintSet.setVerticalBias(view.getId(), 0.26f);
        constraintSet.applyTo(constraintLayout);
    }

    private void countDownTime(){
        currentTimeLeftInTheLevel_MILLIS = currentTimeLeftInTheLevel_MILLIS -DELAY_MILLIS;
        updateScreen();
    }

    @Override
    public void run() {
        if(viewHasBeenCreated) {
            countDownTime();
        }
}
}

不幸的是,此代码导致“java.lang.NullPointerException:尝试在空对象引用上调用虚拟方法 'boolean android.content.Context.isUiContext()'”。它由 View view = new View(getActivity()); 行抛出。这是完整的错误消息:

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.game, PID: 12176
    java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.content.Context.isUiContext()' on a null object reference
        at android.view.ViewConfiguration.get(ViewConfiguration.java:502)
        at android.view.View.<init>(View.java:5317)
        at com.example.game.Test.updateScreen(Test.java:72)
        at com.example.game.Test.countDownTime(Test.java:91)
        at com.example.game.Test.run(Test.java:97)
        at android.os.Handler.handleCallback(Handler.java:938)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

知道问题出在哪里吗?没有自定义 UI 元素,片段工作正常。

没问题。

在这种情况下,像这样的简单 xml 文件就足够了。让我们在 layout 文件夹中将其命名为 something.xml

<LinearLayout ...>
  <ImageView ...>
</LinearLayout>

在另一个布局 xml 文件中,您可以:

<ConstraintLayout ...>
  <include android:id="@+id/something"" layout="@layout/something" android:layout_width="70dp">
</ConstraintLayout>

Reusing layouts

如果您想获得 children,您可以随时在 Activity 或片段上使用 findViewById 来获得它们。如果您正在使用数据绑定或视图绑定,它会变得更好:它们将显示为 XBinding class 中的字段,这些字段是从 XML 文件

中生成的

您好 VanessaF,请进一步说明您在评论中提出的问题:

<include />

<include /> 标签是一个特殊的 XML 标签,我们可以在 Android XML 布局文件中使用它来指示我们放置 <include/> 我们希望将其替换为通过 <include /> 标签内的 layout 属性确定的其他 XML。

这是一个例子:

考虑layout/example.xml

<TextView
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:text="Hello!"/>

并考虑 layout/parent.xml

<LinearLayout 
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">

  <Button .../>
  <include layout="@layout/example"/>
  <ImageView android:drawable="@drawable/ic_send"/>
</LinearLayout>

每当我在某处使用 R.layout.parent 时(例如在 Activity 中的 setContent 中,将生成的视图如下所示:

<LinearLayout 
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">

  <Button .../>
  <!-- PLEASE NOTICE THAT <include/> IS GONE -->
  <!-- AND HAS BEEN REPLACED WITH THE CONTENTS the specified layout -->
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Hello!"/>
  <ImageView android:drawable="@drawable/ic_send"/>
</LinearLayout>

无需编写 full-blown 自定义视图即可有效 re-using 布局。

注意:您在 <include/> 标签内指定的所有属性将有效地覆盖布局文件内指定的其他属性。让我用一个例子来说明这一点:

再考虑一下layout/example.xml。请注意,这次 TextView 将在高度和宽度上都缩小到文本的大小。

<TextView 
  android:text="Hello!"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  />

并考虑 parent:layout/parent.xml。请注意,我正在设置属性 android:layout_widthandroid:layout_height.

<LinearLayout 
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="vertical">
  <include
    layout="@layout/example"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />
</LinearLayout>

在这种情况下,当 Android 将 @layout/example 的内容替换为 <include/> 时,它还会设置 android:layout_width="match_parent"android:layout_height="match_parent",因为它们是在<include/> 标签有效地忽略了 layout/example.xml 中设置的原始属性(设置为“wrap_content”)

使用 TextView。灯泡可以是左compound drawable. Set the background to a rounded rectangle shape drawable. This can all be specified in XML. See TextView.

如果不需要文本,也可以使用 LayerList 可绘制对象来完成。 (TextView 解决方案也可以在没有文本的情况下使用 - 只需将文本设置为“”或 null。)

<layer-list>
    <item>
        <shape android:shape="rectangle">
            <corners android:radius="5dp" />
            <solid android:color="#FF9800" />
        </shape>
    </item>
    <item
        android:drawable="@drawable/ic_baseline_lightbulb_24"
        android:width="48dp"
        android:height="48dp"
        android:gravity="left|center_vertical" />
</layer-list>

图层列表设置为简单View的背景。

<View
    android:layout_width="250dp"
    android:layout_height="56dp"
    android:layout_marginStart="16dp"
    android:layout_marginTop="16dp"
    android:layout_marginEnd="16dp"
    android:layout_marginBottom="16dp"
    android:background="@drawable/light_bulb_layer_list" />

要在代码中创建 View

View view = new View(context);
view.setLayoutParams(new ViewGroup.LayoutParams(width, height));
Drawable dr = ContextCompat.getDrawable(context,R.drawable.light_bulb_layer_list)
view.setBackground(dr);

我建议阅读Custom View Components from the official Android documentation。事实上,对于您使用 Android 应用程序所做的一切,您应该非常熟悉此文档。