Android自定义视图上的onClick方法无法工作

34

我开始开发一个应用程序,昨天我创建了菜单但是onClick方法不起作用!我创建了一个类继承View并将其命名为MainMenuObject - 该类是主菜单中任何对象(按钮、标志等)的通用类。我为它们创建了一个特殊的类,因为当菜单启动时我正在执行动画。建立MainMenuObject类之后,我又建立了另一个类(OpeningTimesView),它继承了View并将主菜单中所有按钮放在其中,并且将作为主Activity的布局。

一切都很好,动画效果非常好,我想在我的按钮上添加监听器,所以我向OpeningTimesView类添加了一个onClickListener的实现,并重写了onClick方法。然后我使用setOnClickListener(this)和setClickable(true)将监听器添加到按钮上,但它不起作用!我尝试了所有方法!请帮助我找出问题出在哪里。我在onClick方法中添加了一个Toast,它不依赖于任何“if”语句,但是它也不显示。

(顺便问一下,有没有办法将屏幕宽度和高度定义为变量,以便所有类都可以访问?它不能是静态的,因为您从一个display对象获取高度和宽度,但必须还有其他方法)

以下是代码:

public class OpeningTimesView extends View implements OnClickListener{
    private MainMenuObjectView searchButton;
    private MainMenuObjectView supportButton;
    private MainMenuObjectView aboutButton;
    private int screenWidth;
    private int screenHeight;
    public OpeningTimesView(Context context, Display dis) {
        super(context);

        this.screenWidth = dis.getWidth();
        this.screenHeight = dis.getHeight();

        searchButton = new MainMenuObjectView(context, 200, MovingMode.RIGHT, R.drawable.search, dis);
        supportButton = new MainMenuObjectView(context, 400, MovingMode.LEFT, R.drawable.support, dis);
        aboutButton = new MainMenuObjectView(context, 600, MovingMode.RIGHT, R.drawable.about, dis);

        searchButton.setClickable(true);
        supportButton.setClickable(true);
        aboutButton.setClickable(true);

        searchButton.setOnClickListener(this);
        supportButton.setOnClickListener(this);
        aboutButton.setOnClickListener(this);
    }

    @Override
    public void onClick(View view){
        Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
        if(view == searchButton){
            Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
        }
        else if(view == supportButton){
            Toast.makeText(getContext(), "Support button pressed", Toast.LENGTH_SHORT).show();
        }
        else Toast.makeText(getContext(), "About button pressed", Toast.LENGTH_SHORT).show();
    }
    @Override
    public void onDraw(Canvas canvas)
    {
        // Drawing the buttons
        this.searchButton.onDraw(canvas);
        this.aboutButton.onDraw(canvas);
        this.supportButton.onDraw(canvas);
    }

提前感谢,Elad!


请查看此链接,我已经想出了一个解决方案。http://stackoverflow.com/questions/2826576/android-custom-view-onclickevent-with-x-y-locations/20653558#20653558 - Manjunath
11个回答

23

我也遇到了同样的问题 - 我创建了一个自定义视图,在活动中通过调用 v.setOnClickListener(new OnClickListener() {...}); 为其注册新的监听器,但监听器却没有被调用。

在我的自定义视图中,我还覆盖了 public boolean onTouchEvent(MotionEvent event) {...} 方法。问题在于我没有调用 View 类的方法 - super.onTouchEvent(event)。这解决了问题。所以如果你想知道为什么你的监听器没有被调用,那么你可能忘记了调用超类的 onTouchEvent 方法。

这里是一个简单的示例:

private static class CustomView extends View implements View.OnClickListener {
    public CustomView(Context context) {
        super(context);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);   // this super call is important !!!
        // YOUR LOGIC HERE
        return true;
    }

    @Override
    public void onClick(View v) {
        // DO SOMETHING HERE
    }
}

请添加代码以便我们理解。你把调用方法放在哪里了? - JPM
谢谢您的评论 - 我添加了一个简单的例子,希望现在更清楚了。 - maddob

18
在Android中创建自定义控件如果不熟悉UI框架的操作可能会很棘手。如果您还没有阅读以下内容,我建议您先阅读这些内容:http://developer.android.com/guide/topics/ui/declaring-layout.htmlhttp://developer.android.com/guide/topics/ui/custom-components.htmlhttp://developer.android.com/guide/topics/ui/layout-objects.html
请注意,当在XML中声明布局时,元素是嵌套的。这会创建一个布局层次结构,当仅使用Java代码定制组件时,必须自己创建该层次结构。
最有可能的是,您被困在Android的触摸层次结构中。与其他一些流行的移动平台不同,Android从View层次结构的顶部开始传递触摸事件并向下工作。传统上占据层次结构较高级别(Activity和Layouts)的类具有将它们自己未消耗的触摸转发的逻辑。
因此,我建议将您的OpeningTimesView更改为扩展ViewGroup(所有Android布局的超类)或特定布局(LinearLayoutRelativeLayout等),并将按钮添加为其子项。目前,似乎没有定义层次结构(按钮不真正“包含”在容器中,它们只是成员),这可能会混淆事件的实际去向。
下面是两个好处:
  1. 触摸应更自然地流向按钮,从而允许触发单击事件
  2. 您可以利用Android的布局机制来绘制视图,而不是依靠绘制代码完成所有操作。
选择一个布局类作为起点,以帮助您将按钮放置在最终位置。您可以使用Android中的动画框架或自定义绘制代码(如现在所做的)以任何您喜欢的方式对其进行动画处理。按钮的位置和该按钮当前绘制的位置可以非常不同,如果需要,那就是Android 3.0之前的当前动画框架的工作原理...但这是一个单独的问题。您还可以使用AbsoluteLayout,它允许您随意放置和替换对象...但要注意此选项在所有Android设备上的外观。
关于显示信息的问题,最简单的方法可能就是在需要的地方使用Context.getResources().getDisplayMetrics()Activity继承自Context,因此可以直接调用此方法。 视图始终具有您可以通过getContext()访问的Context。 对于任何其他类,您只需在构造函数中传递Context作为参数(这是Android中的常见模式,您将看到许多对象需要Context,主要是为了访问Resources)。
以下是一个骨架示例,以启动事情。 这只是将三个水平对齐为最终位置:
Public class OpeningTimesView extends LinearLayout implements OnClickListener {
    private MainMenuObjectView searchButton;
    private MainMenuObjectView supportButton;
    private MainMenuObjectView aboutButton;
    private int screenWidth;
    private int screenHeight;

    public OpeningTimesView(Context context) {
        this(context, null);
    }

    //Thus constructor gets used if you ever instantiate your component from XML
    public OpeningTimesView(Context context, AttributeSet attrs) {
        super(context, attrs);

        /* This is a better way to obtain your screen info
        DisplayMetrics display = context.getResources().getDisplayMetrics();
        screenWidth = display.widthPixels;
        screenHeight = display.heightPixels;
        */
        //This way works also, without needing to customize the constructor
        WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
        Display dis = wm.getDefaultDisplay();
        screenWidth = dis.getWidth();
        screenHeight = dis.getHeight();

        searchButton = new MainMenuObjectView(context, 200, MovingMode.RIGHT, R.drawable.search, dis);
        supportButton = new MainMenuObjectView(context, 400, MovingMode.LEFT, R.drawable.support, dis);
        aboutButton = new MainMenuObjectView(context, 600, MovingMode.RIGHT, R.drawable.about, dis);

        //Even if they don't extend button, if MainMenuObjectView is always clickable
        // this should probably be brought into that class's constructor
        searchButton.setClickable(true);
        supportButton.setClickable(true);
        aboutButton.setClickable(true);

        searchButton.setOnClickListener(this);
        supportButton.setOnClickListener(this);
        aboutButton.setOnClickListener(this);

        //Add the buttons to the layout (the buttons are now children of the container)
        setOrientation(LinearLayout.HORIZONTAL);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
        addView(searchButton, params);
        addView(supportButton, params);
        addView(aboutButton, params);       
    }

    @Override
    public void onClick(View view){
        Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
        if(view == searchButton){
            Toast.makeText(getContext(), "Search button pressed", Toast.LENGTH_SHORT).show();
        }
        else if(view == supportButton){
            Toast.makeText(getContext(), "Support button pressed", Toast.LENGTH_SHORT).show();
        }
        else Toast.makeText(getContext(), "About button pressed", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDraw(Canvas canvas)
    {
        //Drawing the buttons
        // This may only be necessary until they are in place, then just call super.onDraw(canvas)
        this.searchButton.onDraw(canvas);
        this.aboutButton.onDraw(canvas);
        this.supportButton.onDraw(canvas);
    }    
}

你可以从那里自定义它。也许可以将按钮的可见性设置为View.INVISIBLE,直到你使用绘图代码或者自定义的Animation对象使它们动画显示在最终位置上。
然而,关键是布局足够智能,在收到触摸事件时会将其转发给相应的子视图。你可以创建一个没有这个功能的自定义视图,但是你必须拦截容器上的所有触摸事件,并计算出要手动转发事件到哪个子视图。如果你真的无法使任何布局管理器起作用,这是你的后备方案。
希望能对你有所帮助!

首先感谢您的回答! 我尝试扩展ViewGroup,但我需要实现onLayout()方法,而我不太清楚它是什么,我试图将其留空,但当我运行应用程序时,屏幕变成了黑色。 我无法使用任何特定的布局,因为我正在进行动画,按钮和标题不是静态的。我不明白如何使按钮成为子元素而不仅仅是成员。如果您能更详细地解释一下,我会很高兴,因为我对Android还很陌生 :) 至于屏幕大小,谢谢,这帮了我很多! - Elad92
尝试添加一些更多的信息,希望能够在Android上下文中澄清构建组件。一旦您熟悉Android如何管理UI,自定义Android组件就不会那么麻烦了。干杯。 - devunwired
1
谢谢,太棒了。这篇文章非常有用,对我来说真是太好了。我很惊讶为什么它没有得到更多的点赞。 - Sina R.

11

在您的自定义视图的 onTouchEvent 中,您只需要调用 performClick()。

在您的自定义视图中使用以下代码:

    @Override
    public boolean onTouchEvent(final MotionEvent event) {
        if(event.getAction() == MotionEvent.ACTION_UP){
            return performClick();
        }
        return true;
    }

9
我这样做的原因是:
public class YourView extends LinearLayout implements OnClickListener {
    OnClickListener listener;

    //... constructors

    public void setOnClickListener(OnClickListener listener) {
        this.listener = listener;
    }

    @Override
    public void onClick(View v) {
        if (listener != null)
             listener.onClick(v);
    }
}

这似乎很有效。当我在实现自定义ViewGroup时,我发现onClick()可以在我的ViewGroup内触发,但在活动外部却无法触发。所以我采用了这种方法。有效。注意:我无法让butterknife与此一起使用。 - Someone Somewhere

9

在构造函数中调用setOnClickListener(this),并实现自己的View.OnClickListener

具体方法如下:

public class MyView extends View implements View.OnClickListener {

    public MyView(Context context) {
        super(context);
        setOnClickListener(this);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Toast.makeText(getContext(), "On click.", Toast.LENGTH_SHORT).show();
    }
}

8

我遇到了同样的问题。我的情况是将LinearLayout作为自定义视图的根元素,并且将clickablefocusable设置为true,还将自定义视图标签(在碎片布局中使用)设置为可clickablefocusable。结果发现,让它工作的唯一方法就是从XML中删除所有的clickablefocusable属性:) 这虽然与直觉相反,但它确实起作用。


3
在MainMenuObjectView类中实现onClickListener,因为这些对象将响应点击。另一种选择是扩展Button而不是View,因为你只在其中使用按钮。
更新:完整示例
这是直接将其实现到可点击视图中的想法。有一个TestView类,它扩展了View并覆盖了onDraw,正如你所需要的,也会响应点击。我省略了任何动画实现,因为你已经完成了这部分,并且与ClickListener讨论无关。我在Eclair模拟器中测试过它,结果符合预期(点击后弹出Toast消息)。文件名为Test.java。
package com.aleadam.test;

import android.app.Activity;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.TextView;

public class Test extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LinearLayout ll = new LinearLayout(this);
        ll.setOrientation(LinearLayout.VERTICAL);
        TextView label = new TextView(this);
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
             LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        label.setText("Click the circle!");
        TestView testView = new TestView(this);
        ll.addView(label, layoutParams);
        ll.addView(testView, layoutParams);
        setContentView(ll);
    }
}

文件: TestView.java

package com.aleadam.test;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;

public class TestView extends View implements OnClickListener {
    Context context;

    public TestView(Context context) {
        super(context);
        this.context = context;
        setOnClickListener(this);
    }

    public void onClick(View arg0) {
        Toast.makeText(context, "View clicked.", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onDraw (Canvas canvas) {
        super.onDraw(canvas);
        this.setBackgroundColor(Color.LTGRAY);
        Paint paint = new Paint (Paint.ANTI_ALIAS_FLAG);
        paint.setColor(Color.RED);
        canvas.drawCircle(20, 20, 20, paint);
    }
}

如果你需要一些可点击和一些不可点击的内容,你可以添加一个带有布尔参数的构造函数来确定是否将ClickListener附加到视图中:

public TestView(Context context, boolean clickable) {
    super(context);
    this.context = context;
    if (clickable)
        setOnClickListener(this);
}

这并不会真正改变这里的行为,而监听器设计模式的整个重点在于生成事件的对象不需要是处理它的对象。 - devunwired
@Aleadam,那段代码中的点击事件永远不会触发,因为创建它的总触摸事件没有发送到正确的视图。将点击监听器移动不会解决这个问题。 - devunwired
MainMenuObjectView不仅仅是用来创建按钮的,我为主菜单中的所有内容(包括标题)创建它,因为当你打开应用时,我正在做动画,所以我需要重写onDraw方法并添加move()方法,所以将onClick监听器实现到这个类会是错误的选择。关于让它扩展Button而不是View的另一个选项,我尝试过了,但没有帮助,我想这就像Wireless Designs所说的那样,事件被某个地方捕获了...但无论如何感谢您的帮助! - Elad92
@Elad92,请查看更新后的响应,其中包括一个示例,以便您了解我的意思。 - Aleadam
非常好的解决方案...我用它来为扩展线性布局实现onClick事件监听器。点赞! - Johnny
显示剩余5条评论

3
我有一个解决方案! 这不是为了解决特定问题的解决方案,而是一种全新的方法。 我把这个线程发送给我认识的人,他告诉我要使用Android拥有的Animation SDK(就像Wireless Designs提到的那样),所以我只用一个扩展Activity的类来做主菜单页面,而Animation类提供了许多动画选项。 我要感谢你们俩帮助我,你们真棒。 如果有人遇到与此线程相同的问题或其他问题,我会添加代码:
package elad.openapp;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.Toast;

public class OpeningTimes extends Activity implements OnClickListener{
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    // Disabling the title bar..
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.main);

    // Create the buttons and title objects
    ImageView title = (ImageView)findViewById(R.id.title_main);
    ImageView search = (ImageView)findViewById(R.id.search_button_main);
    ImageView support = (ImageView)findViewById(R.id.support_button_main);
    ImageView about = (ImageView)findViewById(R.id.about_button_main);

    // Setting the onClick listeners
    search.setOnClickListener(this);
    support.setOnClickListener(this);
    about.setOnClickListener(this);

    setButtonsAnimation(title, search, support, about);

}

@Override
public void onClick(View v) {
    if(v.getId()==R.id.search_button_main){
        v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        startActivity(new Intent(this,SearchPage.class));
    }
    else if(v.getId()==R.id.support_button_main){
        v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        Toast.makeText(this, "Coming soon...", Toast.LENGTH_LONG).show();
    }
    else if(v.getId()==R.id.about_button_main){

        v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        Toast.makeText(this, "Coming soon...", Toast.LENGTH_LONG).show();
    }
}

// Setting the animation on the buttons
public void setButtonsAnimation(ImageView title, ImageView search, ImageView support, ImageView about){
    // Title animation (two animations - scale and translate)
    AnimationSet animSet = new AnimationSet(true);

    Animation anim = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f);
    anim.setDuration(750);
    animSet.addAnimation(anim);

    anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f,
            Animation.RELATIVE_TO_SELF, -1.0f, Animation.RELATIVE_TO_SELF, 0.0f);
    anim.setDuration(750);
    animSet.addAnimation(anim);

    title.startAnimation(animSet);

    // Search button animation
    anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, -1.5f, Animation.RELATIVE_TO_SELF, 0.0f,
            Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f);
    anim.setDuration(750);

    search.startAnimation(anim);

    // Support button animation
    anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 1.5f, Animation.RELATIVE_TO_SELF, 0.0f,
            Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f);
    anim.setDuration(750);

    support.startAnimation(anim);

    // About button animation
    anim = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f, Animation.RELATIVE_TO_SELF, 0.0f,
            Animation.RELATIVE_TO_SELF, 3f, Animation.RELATIVE_TO_SELF, 0.0f);
    anim.setDuration(750);

    about.startAnimation(anim);
}
}

3

在我的情况下,我在自定义视图中的父布局使用了RelativeLayout。让它工作的唯一方法是在RelativeLayout中将focusableclickable设置为true。在充气布局后,必须在自定义视图的构造函数中添加以下内容:

View view = inflate(getContext(), R.layout.layout_my_custom_view, this);

view.findViewById(R.id.theparent).setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View view) {
            performClick();
        }
    });

2
这很简单:
public class FancyButton
 extends FrameLayout
 implements View.OnClickListener { ..

    void yourSetupFunction(Context context, @Nullable AttributeSet attrs) {
        ..
        super.setOnClickListener(this); // NOTE THE SUPER
    }

    OnClickListener consumerListener = null;
    @Override
    public void setOnClickListener(@Nullable OnClickListener l) {
        consumerListener = l;
        // DO NOT CALL SUPER HERE
    }

    @Override
    public void onClick(View v) {
        Log.i("dev","perform my custom functions, and then ...");
        if (consumerListener != null) { consumerListener.onClick(v); }
    }
  1. 实现View.OnClickListener接口,从而拥有一个onClick(View v)方法
  2. 在setOnClickListener方法中,“记住”来自外部世界设置的监听器
  3. 将实际监听器设置为我们自己(使用“super.” ...)
  4. 在onClick中进行自定义操作,然后调用外部世界的onClick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接