Sök…


Skapa anpassade vyer

Om du behöver en helt anpassad vy måste du underklassen View (superklassen för alla Android-vyer) och tillhandahålla dina anpassade storlekar ( onMeasure(...) ) och ritning ( onDraw(...) ) metoder:

  1. Skapa ditt anpassade bildskelett: det här är i princip samma för varje anpassad vy. Här skapar vi skelettet för en anpassad vy som kan rita en smiley, kallad SmileyView :

    public class SmileyView extends View {
        private Paint mCirclePaint;
        private Paint mEyeAndMouthPaint;
    
        private float mCenterX;
        private float mCenterY;
        private float mRadius;
        private RectF mArcBounds = new RectF();
    
        public SmileyView(Context context) {
            this(context, null, 0);
        }
    
        public SmileyView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initPaints();
        }
    
        private void initPaints() {/* ... */}
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {/* ... */}
    
        @Override
        protected void onDraw(Canvas canvas) {/* ... */}
    }
    
  2. Initialisera dina färger: Paint är penslarna på din virtuella duk som definierar hur dina geometriska föremål återges (t.ex. färg, fyllning och strejkstil osv.). Här skapar vi två Paint , en gulfylld färg för cirkeln och en svart målarfärg för ögonen och munnen:

    private void initPaints() {
        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setStyle(Paint.Style.FILL);
        mCirclePaint.setColor(Color.YELLOW);
        mEyeAndMouthPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mEyeAndMouthPaint.setStyle(Paint.Style.STROKE);
        mEyeAndMouthPaint.setStrokeWidth(16 * getResources().getDisplayMetrics().density);
        mEyeAndMouthPaint.setStrokeCap(Paint.Cap.ROUND);
        mEyeAndMouthPaint.setColor(Color.BLACK);
    }
    
  3. Implementera din egen onMeasure(...) -metod: detta krävs så att överordnade layouter (t.ex. FrameLayout ) kan anpassa din anpassade vy korrekt. Den tillhandahåller en uppsättning av measureSpecs som du kan använda för att bestämma visningens höjd och bredd. Här skapar vi en kvadrat genom att se till att höjden och bredden är desamma:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int w = MeasureSpec.getSize(widthMeasureSpec);
        int h = MeasureSpec.getSize(heightMeasureSpec);
    
        int size = Math.min(w, h);
        setMeasuredDimension(size, size);
    }
    

    Observera att onMeasure(...) måste innehålla minst ett samtal till setMeasuredDimension(..) annars kraschar din anpassade vy med en IllegalStateException .

  4. Implementera din egen onSizeChanged(...) -metod: detta gör att du kan fånga den aktuella höjden och bredden på din anpassade vy för att korrekt justera din renderingskod. Här beräknar vi bara vårt centrum och vår radie:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        mCenterX = w / 2f;
        mCenterY = h / 2f;
        mRadius = Math.min(w, h) / 2f;
    }
    
  5. Implementera din egen onDraw(...) -metod: det är här du implementerar den faktiska rendering av din vy. Det ger ett Canvas objekt som du kan rita på (se den officiella Canvas dokumentationen för alla tillgängliga ritningsmetoder).

    @Override
    protected void onDraw(Canvas canvas) {
        // draw face
        canvas.drawCircle(mCenterX, mCenterY, mRadius, mCirclePaint);
        // draw eyes
        float eyeRadius = mRadius / 5f;
        float eyeOffsetX = mRadius / 3f;
        float eyeOffsetY = mRadius / 3f;
        canvas.drawCircle(mCenterX - eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint);
        canvas.drawCircle(mCenterX + eyeOffsetX, mCenterY - eyeOffsetY, eyeRadius, mEyeAndMouthPaint);
        // draw mouth
        float mouthInset = mRadius /3f;
        mArcBounds.set(mouthInset, mouthInset, mRadius * 2 - mouthInset, mRadius * 2 - mouthInset);
        canvas.drawArc(mArcBounds, 45f, 90f, false, mEyeAndMouthPaint);
    }
    
  6. Lägg till din anpassade vy i en layout: den anpassade vyn kan nu inkluderas i alla layoutfiler som du har. Här FrameLayout vi det bara in i en FrameLayout :

    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.example.app.SmileyView
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    

Observera att det rekommenderas att bygga ditt projekt när visningskoden är klar. Utan att bygga det kommer du inte att kunna se vyn på en förhandsgranskning i Android Studio.

Efter att ha satt ihop allt, bör du hälsas med följande skärm efter att du har startat aktiviteten med ovanstående layout:

SmileyView används inuti aktivitet

Lägga till attribut till vyer

Anpassade vyer kan också ta anpassade attribut som kan användas i resursfiler för Android-layout. För att lägga till attribut i din anpassade vy måste du göra följande:

  1. Definiera namnet och typen av dina attribut: detta görs inom res/values/attrs.xml (skapa det vid behov). Följande fil definierar ett färgattribut för vår smileys ansiktsfärg och ett enumattribut för smileys uttryck:

    <resources>
        <declare-styleable name="SmileyView">
            <attr name="smileyColor" format="color" />
            <attr name="smileyExpression" format="enum">
                <enum name="happy" value="0"/>
                <enum name="sad" value="1"/>
            </attr>
        </declare-styleable>
        <!-- attributes for other views -->
    </resources>
    
  2. Använd dina attribut i din layout: detta kan göras i alla layoutfiler som använder din anpassade vy. Följande layoutfil skapar en skärm med en glad gul smiley:

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_height="match_parent"
        android:layout_width="match_parent">
        
        <com.example.app.SmileyView
            android:layout_height="56dp"
            android:layout_width="56dp"
            app:smileyColor="#ffff00"
            app:smileyExpression="happy" />
    </FrameLayout>
    

    Tips: Anpassade attribut fungerar inte med tools: prefixet i Android Studio 2.1 och äldre (och eventuellt i framtida versioner). I detta exempel ersätter app:smileyColor med tools:smileyColor skulle resultera i att smileyColor varken ställs in under körning eller vid designtid.

  3. Läs dina attribut: detta görs i din anpassade visningskällkod. Följande fragment av SmileyView visar hur attributen kan extraheras:

    public class SmileyView extends View {
        // ...
    
        public SmileyView(Context context) {
            this(context, null);
        }
    
        public SmileyView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SmileyView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, 0);
            mFaceColor = a.getColor(R.styleable.SmileyView_smileyColor, Color.TRANSPARENT);
            mFaceExpression = a.getInteger(R.styleable.SmileyView_smileyExpression, Expression.HAPPY);
            // Important: always recycle the TypedArray
            a.recycle();
    
            // initPaints(); ...
        }
    }
    
  4. (Valfritt) Lägg till standardstil: detta görs genom att lägga till en stil med standardvärden och ladda den i din anpassade vy. Följande standard smiley stil representerar en lycklig gul:

    <!-- styles.xml -->
    <style name="DefaultSmileyStyle">
        <item name="smileyColor">#ffff00</item>
        <item name="smileyExpression">happy</item>
    </style>
    

    Som tillämpas i vår SmileyView genom att lägga till den som den sista parametern i samtalet för att få obtainStyledAttributes (se kod i steg 3):

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SmileyView, defStyleAttr, R.style.DefaultSmileyViewStyle);
    

    Observera att alla attributvärden som ställts in i den uppblåsta layoutfilen (se koden i steg 2) kommer att åsidosätta motsvarande värden i standardformatet.

  5. (Valfritt) Ange stilar inom teman: detta görs genom att lägga till ett nytt stilreferensattribut som kan användas i dina teman och ge en stil för det attributet. Här smileyStyle vi helt enkelt vårt referensattribut smileyStyle :

    <!-- attrs.xml -->
    <attr name="smileyStyle" format="reference" />
    

    Som vi sedan tillhandahåller en stil för i vårt apptema (här använder vi bara standardstilen från steg 4):

    <!-- themes.xml -->
    <style name="AppTheme" parent="AppBaseTheme">
        <item name="smileyStyle">@style/DefaultSmileyStyle</item>
    </style>
    

Skapa en sammansatt vy

En sammansatt vy är en anpassad ViewGroup som behandlas som en enda vy med den omgivande programkoden. En sådan ViewGroup kan vara riktigt användbar i DDD- liknande design, eftersom den kan motsvara ett aggregerat, i detta exempel, en kontakt. Det kan återanvändas överallt där kontakten visas.

Detta innebär att den omgivande styrenhetskoden, en aktivitet, fragment eller adapter, helt enkelt kan överföra dataobjektet till vyn utan att dela den i ett antal olika UI-widgetar.

Detta underlättar återanvändning av koder och ger en bättre design enligt SOLID-förutsättningar .

Layouten XML

Det är vanligtvis där du börjar. Du har en befintlig bit XML som du tycker att du återanvänder, kanske som en <include/> . Extrahera den till en separat XML-fil och linda in rottaggen i ett <merge> -element:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

        <ImageView
            android:id="@+id/photo"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:layout_alignParentRight="true" />

        <TextView
            android:id="@+id/name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_toLeftOf="@id/photo" />

        <TextView
            android:id="@+id/phone_number"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@id/name"
            android:layout_toLeftOf="@id/photo" />
</merge>

Denna XML-fil fortsätter att fungera i Layout Editor i Android Studio. Du kan behandla det som alla andra layouter.

Det sammansatta ViewGroup

När du har XML-filen skapar du den anpassade visningsgruppen.

import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.ImageView;
import android.widget.TextView;

import myapp.R;

/**
 * A compound view to show contacts.
 *
 * This class can be put into an XML layout or instantiated programmatically, it
 * will work correctly either way.
 */
public class ContactView extends RelativeLayout {

    // This class extends RelativeLayout because that comes with an automatic
    // (MATCH_PARENT, MATCH_PARENT) layout for its child item. You can extend
    // the raw android.view.ViewGroup class if you want more control. See the
    // note in the layout XML why you wouldn't want to extend a complex view
    // such as RelativeLayout.

    // 1. Implement superclass constructors.
    public ContactView(Context context) {
        super(context);
        init(context, null);
    }

    // two extra constructors left out to keep the example shorter

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ContactView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context, attrs);
    }

    // 2. Initialize the view by inflating an XML using `this` as parent
    private TextView mName;
    private TextView mPhoneNumber;
    private ImageView mPhoto;

    private void init(Context context, AttributeSet attrs) {
        LayoutInflater.from(context).inflate(R.layout.contact_view, this, true);
        mName = (TextView) findViewById(R.id.name);
        mPhoneNumber = (TextView) findViewById(R.id.phone_number);
        mPhoto = (ImageView) findViewById(R.id.photo);
    }

    // 3. Define a setter that's expressed in your domain model. This is what the example is
    //    all about. All controller code can just invoke this setter instead of fiddling with
    //    lots of strings, visibility options, colors, animations, etc. If you don't use a
    //    custom view, this code will usually end up in a static helper method (bad) or copies 
    //    of this code will be copy-pasted all over the place (worse).
    public void setContact(Contact contact) {
        mName.setText(contact.getName());
        mPhoneNumber.setText(contact.getPhoneNumber());
        if (contact.hasPhoto()) {
            mPhoto.setVisibility(View.VISIBLE);
            mPhoto.setImageBitmap(contact.getPhoto());
        } else {
            mPhoto.setVisibility(View.GONE);
        }
    }
}

Metoden init(Context, AttributeSet) är där du skulle läsa alla anpassade XML-attribut som förklaras i Lägga till attribut till vyer .

Med dessa bitar på plats kan du använda den i din app.

Användning i XML

Här är ett exempel fragment_contact_info.xml som illustrerar hur du skulle lägga en enda ContactView ovanpå en lista med meddelanden:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- The compound view becomes like any other view XML element -->
    <myapp.ContactView
        android:id="@+id/contact"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/message_list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>

</LinearLayout>

Användning i kod

Här är ett exempel RecyclerView.Adapter som visar en lista över kontakter. Det här exemplet illustrerar hur mycket renare styrenhetskoden får när den är helt fri från Visa-manipulation.

package myapp;

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

public class ContactsAdapter extends RecyclerView.Adapter<ContactsViewHolder> {

    private final Context context;

    public ContactsAdapter(final Context context) {
        this.context = context;
    }

    @Override
    public ContactsViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ContactView v = new ContactView(context); // <--- this
        return new ContactsViewHolder(v);
    }

    @Override
    public void onBindViewHolder(ContactsViewHolder holder, int position) {
        Contact contact = this.getItem(position);
        holder.setContact(contact);  // <--- this
    }

    static class ContactsViewHolder extends RecyclerView.ViewHolder {

        public ContactsViewHolder(ContactView itemView) {
            super(itemView);
        }

        public void setContact(Contact contact) {
            ((ContactView) itemView).setContact(contact); // <--- this
        }
    }
}

CustomView-prestandatips

Tilldela inte nya objekt i onDraw

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Paint paint = new Paint(); //Do not allocate here
}

Istället för att rita veckor i duk ...

drawable.setBounds(boundsRect);

drawable.draw(canvas);

Använd en bitmapp för snabbare ritning:

canvas.drawBitmap(bitmap, srcRect, boundsRect, paint);

Rita inte om hela vyn för att uppdatera bara en liten del av den. Istället rita den specifika delen av vyn.

invalidate(boundToBeRefreshed);

Om din åsikt gör en kontinuerlig animation, till exempel ett vaktapp som visar varje sekund, åtminstone stoppa animationen på onStop() av aktiviteten och starta den på onStart() av aktiviteten.

Gör inga beräkningar i onDraw metoden i en vy, du bör istället avsluta ritningen innan du ringer invalidate() . Genom att använda den här tekniken kan du undvika att ram tappar i din vy.

rotationer

De grundläggande operationerna i en vy är översätta, rotera, etc. ... Nästan varje utvecklare har mött detta problem när de använder bitmapp eller lutningar i sin anpassade vy. Om vyn kommer att visa en roterad vy och bitmappen måste roteras i den anpassade vyn, kommer många av oss att tro att det kommer att bli dyrt. Många tycker att det är mycket dyrt att rotera en bitmapp eftersom du måste översätta bitmappens pixelmatris för att kunna göra det. Men sanningen är att den inte är så tuff! I stället för att rotera bitmappen, rotera bara själva duken!

// Save the canvas state
int save = canvas.save();
// Rotate the canvas by providing the  center point as pivot and angle
canvas.rotate(pivotX, pivotY, angle);
// Draw whatever you want
// Basically whatever you draw here will be drawn as per the angle you rotated the canvas
canvas.drawBitmap(...);
// Now restore your your canvas to its original state
canvas.restore(save);
// Unless canvas is restored to its original state, further draw will also be rotated.

Sammansatt vy för SVG / VectorDrawable som drawableRight

Huvudmotivet för att utveckla denna sammansatta vy är att enheter under 5,0 stöder inte svg i dragbara inuti TextView / EditText. En annan proffs är att vi kan ställa in height och widthdrawableRight i EditText . Jag har separerat det från mitt projekt och skapat i en separat modul.

Modulnamn: custom_edit_drawable (kortnamn för prefix- c_d_e)

"c_d_e_" prefix att använda så att appmodulresurserna inte ska åsidosätta dem av misstag. Exempel: prefix "abc" används av google i supportbiblioteket.

build.gradle

dependencies {
   compile 'com.android.support:appcompat-v7:25.3.1'
}

använd AppCompat> = 23

Layoutfil: c_e_d_compound_view.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <EditText
        android:id="@+id/edt_search"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="text"
        android:maxLines="1"
        android:paddingEnd="40dp"
        android:paddingLeft="5dp"
        android:paddingRight="40dp"
        android:paddingStart="5dp" />

    <!--make sure you are not using ImageView instead of this-->
    <android.support.v7.widget.AppCompatImageView
        android:id="@+id/drawbleRight_search"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_gravity="right|center_vertical"
        android:layout_marginLeft="8dp"
        android:layout_marginRight="8dp" />
</FrameLayout>

Anpassade attribut: attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="EditTextWithDrawable">
        <attr name="c_e_d_drawableRightSVG" format="reference" />
        <attr name="c_e_d_hint" format="string" />
        <attr name="c_e_d_textSize" format="dimension" />
        <attr name="c_e_d_textColor" format="color" />
    </declare-styleable>
</resources>

Kod: EditTextWithDrawable.java

public class EditTextWithDrawable extends FrameLayout {
    public AppCompatImageView mDrawableRight;
    public EditText mEditText;

    public EditTextWithDrawable(Context context) {
        super(context);
        init(null);
    }

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

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

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public EditTextWithDrawable(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        if (attrs != null && !isInEditMode()) {
            LayoutInflater inflater = (LayoutInflater) getContext()
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            inflater.inflate(R.layout.c_e_d_compound_view, this, true);
            mDrawableRight = (AppCompatImageView) ((FrameLayout) getChildAt(0)).getChildAt(1);
            mEditText = (EditText) ((FrameLayout) getChildAt(0)).getChildAt(0);

            TypedArray attributeArray = getContext().obtainStyledAttributes(
                    attrs,
                    R.styleable.EditTextWithDrawable);

            int drawableRes =
                    attributeArray.getResourceId(
                            R.styleable.EditTextWithDrawable_c_e_d_drawableRightSVG, -1);
            if (drawableRes != -1) {
                mDrawableRight.setImageResource(drawableRes);
            }

            mEditText.setHint(attributeArray.getString(
                    R.styleable.EditTextWithDrawable_c_e_d_hint));
            mEditText.setTextColor(attributeArray.getColor(
                    R.styleable.EditTextWithDrawable_c_e_d_textColor, Color.BLACK));
            int textSize = attributeArray.getDimensionPixelSize(R.styleable.EditTextWithDrawable_c_e_d_textSize, 15);
            mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
            android.view.ViewGroup.LayoutParams layoutParams = mDrawableRight.getLayoutParams();
            layoutParams.width = (textSize * 3) / 2;
            layoutParams.height = (textSize * 3) / 2;
            mDrawableRight.setLayoutParams(layoutParams);

            attributeArray.recycle();
        }
    }
}

Exempel: Hur man använder ovanifrån

Layout: Activity_main.xml

<LinearLayout 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="wrap_content"
    android:orientation="vertical">

    <com.customeditdrawable.AppEditTextWithDrawable
        android:id="@+id/edt_search_emp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:c_e_d_drawableRightSVG="@drawable/ic_svg_search"
        app:c_e_d_hint="@string/hint_search_here"
        app:c_e_d_textColor="@color/text_color_dark_on_light_bg"
        app:c_e_d_textSize="@dimen/text_size_small" />
</LinearLayout> 

Aktivitet: MainActivity.java

public class MainActivity extends AppCompatActivity {
    EditTextWithDrawable mEditTextWithDrawable;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mEditTextWithDrawable= (EditTextWithDrawable) findViewById(R.id.edt_search_emp);
    }
}

Svara på Touch-händelser

Många anpassade vyer måste acceptera användarinteraktion i form av berörningshändelser. Du kan få tillgång till beröringshändelser genom att åsidosätta onTouchEvent . Det finns ett antal åtgärder du kan filtrera bort. De viktigaste är

  • ACTION_DOWN : Detta utlöses en gång när fingret först rör vid vyn.
  • ACTION_MOVE : Detta kallas varje gång fingret rör sig lite över vyn. Det kallas många gånger.
  • ACTION_UP : Detta är den sista åtgärden som kallas när du lyfter fingret från skärmen.

Du kan lägga till följande metod i din vy och sedan observera loggutgången när du trycker på och flyttar fingret runt vyn.

@Override
public boolean onTouchEvent(MotionEvent event) {

    int x = (int) event.getX();
    int y = (int) event.getY();
    int action = event.getAction();

    switch (action) {
        case MotionEvent.ACTION_DOWN:
            Log.i("CustomView", "onTouchEvent: ACTION_DOWN: x = " + x + ", y = " + y);
            break;

        case MotionEvent.ACTION_MOVE:
            Log.i("CustomView", "onTouchEvent: ACTION_MOVE: x = " + x + ", y = " + y);
            break;

        case MotionEvent.ACTION_UP:
            Log.i("CustomView", "onTouchEvent: ACTION_UP: x = " + x + ", y = " + y);
            break;
    }
    return true;
}

Vidare läsning:



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow