разработчик приложений под Android
–0,8
рейтинг
26 октября 2015 в 12:17

Разработка → Интеграция нативной рекламы Admob в ListView из песочницы

Весной 2015 года в рамках Google Admob был анонсирован новый для них способ отображения рекламных блоков, а именно Native ads (далее нативная реклама), который на текущий момент действует в режиме бета-релиза. Плюс нативной рекламы – это высокая адаптивность к дизайну приложения, которая позволяет показать рекламу таким образом, чтобы она не бросалась в глаза и не мешала работе с приложением. В статье будут отмечены определенные моменты, которые возникли в процессе интеграции Admob Native Ads в собственное приложение (пример того, что получилось показан на картинке, названия удалены), а также приведен практический пример с фрагментами кода (полный вариант при желании можно загрузить и посмотреть тут.

imageДовольно популярным способом для интеграции native ads в мобильные приложения является отображение блоков рекламы в списке через равное количество элементов данных. Например, в распространенном клиенте myMail в списке входящих через каждые N писем отображается блок рекламы. Подобный опыт не блещет новизной, и ранее был успешно применен в Twitter. Стоит упомянуть, что у Admob есть определенные правила публикации рекламы в мобильных приложениях, несоблюдение которых чревато блокировкой Admob аккаунта. Важный момент — разработчики приложений имеют право отображать не более 3 баннеров рекламы на 1 странице с прокручиваемым контентом, причем так, чтобы в видимой части экрана одновременно отображался только 1 баннер. Эти правила касаются баннеров, и пока неизвестно будут ли приведены новые правила касательно нативной рекламы. Тем не менее стоит это учитывать при разработке.

Итак, процесс отображения нативной рекламы в списке можно разбить на следующие этапы:

  1. Загрузка определенного количества блоков рекламы с сервера Admob и сохранение их в некой коллекции объектов (процесс загрузки рекламы может быть не быстрым, так что лучше иметь несколько блоков рекламы наготове).
  2. Выдача загруженных объектов рекламы адаптеру списка по запросу и, возможно, асинхронная дозагрузка следующего блока.
  3. Определение типа отображаемого элемента списка (реклама или данные) на основе его позиции и общем количестве элементов в адаптере.
  4. Отображение layout рекламы в случае если отображаемый элемент списка можно отобразить как рекламный блок (к примеру, если элемент с подходящей позицией для рекламы и, хотя бы один блок рекламы уже был загружен с Admob).
  5. Публикация данных из рекламного блока в соответствующий layout элемента списка.

0. Подготовка проекта


Создаем новый проект, либо открываем существующий. Сперва к модулю проекта, куда планируется добавить функционал по отображению рекламы, следует подключить com.google.android.gms:play-services-ads, это можно сделать, добавив в build.gradle модуля строку:

dependencies {
    //…
    compile 'com.google.android.gms:play-services-ads:7.8.0'
    //…
}

Возможно для этого придется обновить библиотеку Google Play Services в Android SDK Manager.

1. Загрузка рекламы с Admob


Решив не изобретать свой велосипед, ваш покорный слуга взял за основу функционал для нативной рекламы из библиотеки Yahoo fetchr, заточив ее под Admob, благо библиотека распространяется под лицензией Apache 2.0. Для загрузки блоков рекламы создадим класс AdmobFetcher.java и проинициализируем его поля:

public class AdmobFetcher {
    private static final int PREFETCHED_ADS_SIZE = 2;
    private static final int MAX_FETCH_ATTEMPT = 4;

    private AdLoader adLoader;
    private List<NativeAd> mPrefetchedAdList = new ArrayList<NativeAd>();
    private Map<Integer, NativeAd> adMapAtIndex = new HashMap<Integer, NativeAd>();
    private int mNoOfFetchedAds;
    private int mFetchFailCount;
    private WeakReference<Context> mContext = new WeakReference<Context>(null);
    private String admobReleaseUnitId;
}

Где:
PREFETCHED_ADS_SIZE – максимальное число предварительно загруженных рекламных блоков; MAX_FETCH_ATTEMPT – лимит попыток загрузки рекламы;
adLoader – сущность класса com.google.android.gms.AdLoader, которая отвечает за загрузку рекламы с сервера Admob;
mPrefetchedAdList – коллекция с предзагруженной рекламой; adMapAtIndex – маппинг для получения нужного рекламного блока по индексу;
mNoOfFetchedAds, mFetchFailCount – количество загруженной рекламы и неудачных загрузок соответственно; mContext – контекст, храним его в WeakReference, чтобы избежать изменения, например при ротации девайса (это реализация Yahoo fetchr и было решено оставить как есть);
admobReleaseUnitId – релизный unitId, который используем только для публикации релиза. Предположительно сгенерировать его можно будет в консоли Admob, так же как и для баннерной рекламы, а пока что используем указанный в мануале тестовый unitId.

Важный момент – во время отладки настойчиво рекомендуется использовать именно тестовый unitId, в противном случае Admob может счесть клики по рекламным блокам за накрутку со всеми вытекающими последствиями.

Добавим в класс метод инициализации adLoader:

Развернуть
private synchronized void setupAds() {
//муки выбора unitId.
        String admobUnitId = mContext.get().getResources().getString(R.string.test_admob_unit_id);
        // String admobUnitId = mAdmobReleaseUnitId;
        adLoader = new AdLoader.Builder(mContext.get(), admobUnitId)
                .forAppInstallAd(new NativeAppInstallAd.OnAppInstallAdLoadedListener() {
                    @Override
                    public void onAppInstallAdLoaded(NativeAppInstallAd appInstallAd) {
                        onAdFetched(appInstallAd);
                    }
                })
                .forContentAd(new NativeContentAd.OnContentAdLoadedListener() {
                    @Override
                    public void onContentAdLoaded(NativeContentAd contentAd) {
                        onAdFetched(contentAd);
                    }
                })
                .withAdListener(new AdListener() {
                    @Override
                    public void onAdFailedToLoad(int errorCode) {
                        mFetchFailCount++; //инкрементим кол-во неудачных загрузок
                        ensurePrefetchAmount(); //проверяем, что предварительно загружено 
// достаточное количество рекламы
                    }
                })
                .withNativeAdOptions(new NativeAdOptions.Builder().build()).build();
}


Вкратце по методу, сперва получаем unitId, в отладке используем только тестовый, для этого добавим в res\values\strings.xml следующую строку:

<string name="test_admob_unit_id">ca-app-pub-3940256099942544/2247696110</string>

Затем создаем объект adLoader. На текущий момент Admob имеет два типа нативной рекламы, отличающихся внешним видом и структурой: NativeAppInstallAd — суть предложение установить некое стороннее приложение, и NativeContentAd – это обычная реклама с неким контентом. Оба типа унаследованы от базового NativeAd. Обрабатываем получение обоих типов рекламы в вызовах forAppInstallAd и в forContentAd. Возможные ошибки в ходе загрузки следует обработать в вызове withAdListener, передавая объект типа AdListener с перегруженным onAdFailedToLoad. Вызов withNativeAdOptions дает возможность задавать специфические параметры для загрузки, например, вместо картинок принимать только Url (по умолчанию выбран вариант с загрузкой самих изображений), либо предпочтительную ориентацию картинок. Детально с этими методами и типами можно ознакомиться опять-таки в вышеуказанном мануале.

Метод, вызываемый в случае успешной загрузки:

private synchronized void onAdFetched(NativeAd adNative) {
        if (canUseThisAd(adNative)) { 
            mPrefetchedAdList.add(adNative); 
//инкрементим счетчик загруженной рекламы
            mNoOfFetchedAds++;
        }
//сбрасываем счетчик неудачных загрузок
        mFetchFailCount = 0;
        ensurePrefetchAmount();
// оповещаем подписчиков об изменении количества  загруженной рекламы
        notifyObserversOfAdSizeChange();    
}

По сути, в canUseThisAd мы проверяем корректную ли информацию получили в объекте NativeAd и добавляем ее в коллекцию предзагруженных mPrefetchedAdList. Далее идет игра со счетчиками mNoOfFetchedAds и mFetchFailCount, а в ensurePrefetchAmount проверяем, достаточное ли количество рекламы было предварительно загружено. Приведу код вызываемых выше методов:

Развернуть
//оповещение всех подписчиков о изменении количества загруженной рекламы
private void notifyObserversOfAdSizeChange() {
        for (AdmobListener listener : mAdNativeListeners) {
            listener.onAdCountChanged();
        }
}

//проверяем, достаточно ли рекламы загрузили, и если нет, то загружаем еще,
//в случае превышения числа неудачных попыток ничего не делаем 
private synchronized void ensurePrefetchAmount() {
        if (mPrefetchedAdList.size() < PREFETCHED_ADS_SIZE &&
                (mFetchFailCount < MAX_FETCH_ATTEMPT)) {
            fetchAd();
        }
}

//валидация полученной с сервера рекламы,  пример
private boolean canUseThisAd(NativeAd adNative) {
        if (adNative != null) {
            CharSequence header = null, body = null;
            if (adNative instanceof NativeContentAd) {
                NativeContentAd ad = (NativeContentAd) adNative;
                header = ad.getHeadline();
                body = ad.getBody();
            } else if (adNative instanceof NativeAppInstallAd) {
                NativeAppInstallAd ad = (NativeAppInstallAd) adNative;
                header = ad.getHeadline();
                body = ad.getBody();
            }
//тут крайне привередливо проверяем, подходит ли нам эта реклама :)
            return !TextUtils.isEmpty(header)&& !TextUtils.isEmpty(body); 
            }
        }
        return false;
}


Напишем реализацию загрузки рекламы:

Развернуть
//публичный метод для инициализации и первой загрузки 
public synchronized void prefetchAds(Context context) {
        mContext = new WeakReference<Context>(context);
        setupAds(); //инициализируем (см. выше)
        fetchAd(); //загружаем первый блок
    }

    private synchronized void fetchAd() {
        Context context = mContext.get();
        if (context != null) {
//загружаем блок
            adLoader.loadAd(getAdRequest());            
        } else {
            mFetchFailCount++;
	//контекст пустой, считаем за неудачную попытку
        }
}


Метод getAdRequest реализован для построения запроса AdRequest при помощи AdRequest.Builder. С развитием нативной рекламы Admob возможно будет расширен вызовом adBldr.addTestDevice(<ID девайса>), чтобы исключать из реального unitId некоторые тестовые девайсы, как это уже реализовано в баннерной рекламе. К слову, в данный момент вызов addTestDevice игнорируется, как мне было указано тут.

private synchronized AdRequest getAdRequest() {
        AdRequest.Builder adBldr = new AdRequest.Builder();
                //.addTestDevice(AdRequest.DEVICE_ID_EMULATOR);
                // все эмуляторы отмечаются как тестовые для unitId
        return adBldr.build();
}

2. Выдача загруженных объектов рекламы адаптеру списка


И, пожалуй, самый важный метод в AdmobFetcher.java – это метод получения рекламного блока по внешнему индексу, которым оперирует адаптер списка (чуть далее этот секрет будет раскрыт).

Развернуть
public synchronized NativeAd getAdForIndex(final int index) {
        //пытаемся получить рекламный блок по индексу из маппинга
        NativeAd adNative = adMapAtIndex.get(index);
        //если в маппинге по указанному индексу ничего не нашлось, и хотя бы один блок рекламы уже был загружен
        if (adNative == null && mPrefetchedAdList.size() > 0) {
            //забираем первый загруженный блок из коллекции
            adNative = mPrefetchedAdList.remove(0);
            //и пишем его в маппинг по указанному индексу, для будущих запросов
            if (adNative != null) {
                adMapAtIndex.put(index, adNative);
            }
        }
        // проверяем, достаточно ли рекламы загружено
        ensurePrefetchAmount(); 
        return adNative;
}


Маппинг используется для связи внешних индексов и блоков рекламы. Также добавим метод подписки на изменение количества загруженной рекламы:

public synchronized void addListener(AdmobListener listener) {
        mAdNativeListeners.add(listener);
}

И создадим соответствующий интерфейс для подписчиков:

public interface AdmobListener {
        void onAdCountChanged();
}

3. Определение типа отображаемого элемента списка в адаптере


На данном этапе необходимо создать адаптер списка с функционалом для отображения рекламных блоков через равное количество элементов. Чтобы избежать смешивания рекламного кода с логикой отображения данных в списке, которая сама по себе может быть достаточно сложной, имеет смысл реализовать их в различных классах. Возможно, кто-то предпочтет наследование, мной же была выбрана агрегация и создан адаптер-обертка AdmobAdapterWrapper, для того, чтобы абстрагироваться от источника данных и логики их отображения.

На практике это будет выглядеть так: во-первых, создаем экземпляр адаптера, который привязывает список к источнику данных, например, ABCListAdapter; во-вторых, создаем экземпляр AdmobAdapterWrapper для отображения в списке рекламы; в-третьих, передаем в AdmobAdapterWrapper ссылку на созданный экземпляр ABCListAdapter; в-четвертых, указываем экземпляр AdmobAdapterWrapper в качестве адаптера для элемента представления ListView. Таким образом, при любом вызове notifyDataSetChanged в адаптере ABCListAdapter обертка AdmobAdapterWrapper также сможет вызвать notifyDataSetChanged и отобразить рекламу в элементе представления. Итак, создадим класс адаптера списка AdmobAdapterWrapper.java:

Развернуть
public class AdmobAdapterWrapper extends BaseAdapter implements AdmobFetcher.AdmobListener {
    private BaseAdapter mAdapter;

    public BaseAdapter getAdapter() {
        return mAdapter;
    }

    public void setAdapter(BaseAdapter adapter) {
        mAdapter = adapter;
        mAdapter.registerDataSetObserver(new DataSetObserver() {
            @Override
            public void onChanged() {
                notifyDataSetChanged();
            }

            @Override
            public void onInvalidated() {
                notifyDataSetInvalidated();
            }
        });
    }

    AdmobFetcher adFetcher;
    Context mContext;

    private final static int VIEW_TYPE_COUNT = 2;
    private final static int VIEW_TYPE_AD_CONTENT = 1;
    private final static int VIEW_TYPE_AD_INSTALL = 2;
    private final static int DEFAULT_NO_OF_DATA_BETWEEN_ADS = 10;
    private final static int DEFAULT_LIMIT_OF_ADS = 3;

    private int mNoOfDataBetweenAds;

    public int getNoOfDataBetweenAds() {
        return mNoOfDataBetweenAds;
    }

    public void setNoOfDataBetweenAds(int mNoOfDataBetweenAds) {
        this.mNoOfDataBetweenAds = mNoOfDataBetweenAds;
    }

    private int mLimitOfAds;

    public int getLimitOfAds() {
        return mLimitOfAds;
    }

    public void setLimitOfAds(int mLimitOfAds) {
        this.mLimitOfAds = mLimitOfAds;
    }

    private int mContentAdsLayoutId;

    public int getContentAdsLayoutId() {
        return mContentAdsLayoutId;
    }

    public void setContentAdsLayoutId(int mContentAdsLayoutId) {
        this.mContentAdsLayoutId = mContentAdsLayoutId;
    }

    private int mInstallAdsLayoutId;

    public int getInstallAdsLayoutId() {
        return mInstallAdsLayoutId;
    }

    public void setInstallAdsLayoutId(int mInstallAdsLayoutId) {
        this.mInstallAdsLayoutId = mInstallAdsLayoutId;
    }

    public AdmobAdapterWrapper(Context context) {
        setNoOfDataBetweenAds(DEFAULT_NO_OF_DATA_BETWEEN_ADS);
        setLimitOfAds(DEFAULT_LIMIT_OF_ADS);
        setContentAdsLayoutId(R.layout.adcontentlistview_item);
        setInstallAdsLayoutId(R.layout.adinstalllistview_item);
        mContext = context;

        adFetcher = new AdmobFetcher();
        adFetcher.addListener(this);
        // Загружаем первый блок рекламы
        adFetcher.prefetchAds(context);
    }

    @Override
    public void onAdCountChanged() {
        notifyDataSetChanged();
    }
}


AdmobAdapterWrapper реализует интерфейс AdmobFetcher.AdmobListener, соответственно в конструкторе мы создаем экземпляр AdmobFetcher, а затем подписываемся на изменение количества загруженной рекламы таким образом:

adFetcher = new AdmobFetcher();
adFetcher.addListener(this);

Рассмотрим некоторые поля класса:

mAdapter:BaseAdapter – поле для агрегации адаптера данных, в нашем примере это экземпляр ABCListAdapter, имеет get- и set-accessor. В set-acessor после записи в поле подписываемся на изменения в адаптере данных;
adFetcher:AdmobFetcher – объект, который отвечает за загрузку рекламы с сервера Admob; VIEW_TYPE_COUNT:int – количество типов рекламных блоков, как было указано ранее, в настоящее время равен 2;
VIEW_TYPE_AD_CONTENT:int – идентификатор типа рекламы с контентом;
VIEW_TYPE_AD_INSTALL:int — идентификатор типа рекламы для установки приложений;
mNoOfDataBetweenAds:int — интервал (количество) элементов данных, через которые отображать рекламные блоки и его значение по умолчанию DEFAULT_NO_OF_DATA_BETWEEN_ADS:int, равное 10;
mLimitOfAds:int – максимальное число рекламных блоков в списке и его значение по умолчанию DEFAULT_LIMIT_OF_ADS:int, равное 3 (опять же, правила и рекомендации Admob);
mContentAdsLayoutId:int и mInstallAdsLayoutId:int – это layout id для рекламы с контентом и для рекламы установки приложений соответственно (подробнее о них написано тут). Layout id для рекламных блоков будут рассмотрены несколько позже. В конце приведен конструктор класса и реализация для AdmobFetcher.AdmobListener.

Для того, чтобы определять тип отображаемого элемента (реклама или данные) добавим следующие методы:

Развернуть
@Override
public int getCount() {
        if (mAdapter != null) {
            int noOfAds = getAdsCountToPublish();
            return mAdapter.getCount() > 0 ? mAdapter.getCount() + noOfAds : 0;
        } else {
            return 0;
        }
}

public int getAdsCountToPublish(){
        int noOfAds = Math.min(adFetcher.getFetchedAdsCount(),
                mAdapter.getCount() / getNoOfDataBetweenAds());
        return Math.min(noOfAds, getLimitOfAds());
}

@Override
public Object getItem(int position) {
        if (canShowAdAtPosition(position)) {
//вычисляем порядковый индекс рекламы по позиции элемента в списке
//например: если реклама идет через каждые 5 элементов, то при position=11 
//adPos будет равен 1
            int adPos = getAdIndex(position);
            return adFetcher.getAdForIndex(adPos);
        } else {
            int origPos = getOriginalContentPosition(position);
            return mAdapter.getItem(origPos);
        }
}

@Override
public long getItemId(int position) {
        return position;
}

@Override
public int getViewTypeCount() {
        return VIEW_TYPE_COUNT + getAdapter().getViewTypeCount();
}

@Override
public int getItemViewType(int position) {
        if (canShowAdAtPosition(position)) {
            int adPos = getAdIndex(position);
            NativeAd ad = adFetcher.getAdForIndex(adPos);
            return ad instanceof NativeAppInstallAd ? VIEW_TYPE_AD_INSTALL : VIEW_TYPE_AD_CONTENT;
        } else {
            int origPos = getOriginalContentPosition(position);
            return mAdapter.getItemViewType(origPos);
        }
}

protected int getOriginalContentPosition(int position) {
        int noOfAds = getAdsCountToPublish();
        // No of spaces for ads in the dataset, according to ad placement rules
//число элементов, отведенных под рекламу, соответственно с параметрами ее размещения 
        int adSpacesCount = position / (getNoOfDataBetweenAds() + 1);
        return position - Math.min(adSpacesCount, noOfAds);
}

protected boolean canShowAdAtPosition(int position) {
        // корректный ли элемент отображения, чтобы опубликовать в нем рекламный блок?
        // Доступна ли реклама для этого индекса?
        return isAdPosition(position) && isAdAvailable(position);
}

private int getAdIndex(int position) {
        return (position / getNoOfDataBetweenAds()) - 1;
}

private boolean isAdPosition(int position) {
        return (position + 1) % (getNoOfDataBetweenAds() + 1) == 0;
}

private boolean isAdAvailable(int position) {
        int adIndex = getAdIndex(position);
//не отображаем рекламу на самом первом элементе и прочие проверки
        return position >= getNoOfDataBetweenAds()
                && adIndex >= 0
                && adIndex < getLimitOfAds()
                && adFetcher.getFetchedAdsCount() > adIndex;
}


Где getCount – переопределенный метод класса BaseAdapter, возвращающий общее количество элементов, включая блоки рекламы. К примеру, если в источнике данных 10 элементов и интервал показа рекламы mNoOfDataBetweenAds задан равным 5, то getCount вернет 12.

getAdsCountToPublish – возвращает число рекламных блоков, которые возможно опубликовать в список, как наименьшее из следующих величин: заданного максимального количества рекламных блоков в списке getLimitOfAds(), фактического числа загруженных рекламных блоков в adFetcher и потенциального количества рекламы в списке (т.е. сколько блоков можем разместить в данном списке в принципе, исходя из количества элементов и интервала показа рекламы), как отношение:

mAdapter.getCount() / getNoOfDataBetweenAds()

getItem – переопределенный метод BaseAdapter, в данной реализации проверяет (вызов метода canShowAdAtPosition) можно ли отобразить рекламный блок на указанной позиции position, и если да, то возвращает объект NativeAd из adFetcher, иначе – элемент из источника данных.

Также стоит обратить внимание на то, что перед тем как забрать рекламу или элемент, position преобразуется в соответствующий индекс рекламного блока (getAdIndex) или индекс источника данных (getOriginalContentPosition). getViewTypeCount — переопределенный метод BaseAdapter, вычисляющий количество типов элементов представления в списке как сумму типов элементов представления в mAdapter и двух типов рекламы, Install и Content. getItemViewType – еще один переопределенный метод BaseAdapter, который возвращает тип элемента представления в зависимости от указанной позиции position. getOriginalContentPositionи getAdIndex – методы, преобразующие индекс отображаемого элемента position в исходный индекс элемента из источника данных, как если бы рекламы не было, и в индекс рекламного блока, хранящегося в adFetcher соответственно. canShowAdAtPosition – проверяет можно ли отобразить в элементе ListView с индексом position рекламный блок (isAdPosition), с учетом того, сколько рекламы фактически предзагружено и доступно для отображения(isAdAvailable).

4. Отображение layout рекламы


Создадим новый res\layout\adcontentlistview_item.xml: для отображения рекламы с контентом. Данная разметка не отличается оптимальностью и от лишней вложенности элементов имеет смысл избавиться, однако в качестве тестового примера такой вариант вполне подойдет. Единственный момент, на который стоит обратить внимание – это на корневой элемент NativeContentAdView, который используется для того, чтобы автоматизировать обработку пользовательских кликов или иных действий с блоком рекламы и переложить ее на плечи библиотеки Google Play Services и Admob.

Развернуть
<com.google.android.gms.ads.formats.NativeContentAdView
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="wrap_content">
    <LinearLayout android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="vertical">
        <TextView android:id="@+id/tvHeader"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"/>
        <LinearLayout android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 android:layout_marginTop="2dp">
            <ImageView android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:id="@+id/ivLogo"/>
            <LinearLayout android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 android:layout_marginLeft="5dp">
                <TextView android:id="@+id/tvDescription"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"/>
                <TextView android:id="@+id/tvAdvertiser"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:layout_marginTop="1dp"/>
            </LinearLayout>
        </LinearLayout>
        <ImageView android:layout_width="match_parent"
 android:layout_height="200dp"
 android:id="@+id/ivImage"
 android:scaleType="center"
 android:layout_marginTop="2dp"
 android:background="#00FFFFFF"/>
        <Button android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:paddingLeft="10dp"
 android:paddingRight="10dp"
 android:layout_marginTop="2dp"
 android:id="@+id/btnAction"/>
    </LinearLayout>
</com.google.android.gms.ads.formats.NativeContentAdView>


Аналогично создаем и разметку для типа рекламы с предложением установить приложение res\layout\adinstalllistview_item.xml:

Развернуть
<com.google.android.gms.ads.formats.NativeAppInstallAdView
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="wrap_content">
    <LinearLayout android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="vertical">
        <TextView android:id="@+id/tvHeader"
 android:layout_width="match_parent"
 android:layout_height="wrap_content"/>
        <LinearLayout android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:orientation="horizontal"
 android:layout_marginTop="2dp">
            <ImageView android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:id="@+id/ivLogo"/>
            <LinearLayout android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical"
 android:layout_marginLeft="5dp">
                <TextView android:id="@+id/tvDescription"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"/>
                <RelativeLayout android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:layout_marginTop="1dp">
                    <TextView android:id="@+id/tvStore"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentLeft="true"/>
                    <TextView android:id="@+id/tvPrice"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_alignParentRight="true"/>
                </RelativeLayout>
            </LinearLayout>
        </LinearLayout>
        <ImageView android:layout_width="match_parent"
 android:layout_height="200dp"
 android:id="@+id/ivImage"
 android:scaleType="center"
 android:layout_marginTop="2dp"
 android:background="#00FFFFFF"/>
        <Button android:layout_width="match_parent"
 android:layout_height="wrap_content"
 android:paddingLeft="10dp"
 android:paddingRight="10dp"
 android:layout_marginTop="2dp"
 android:id="@+id/btnAction"/>
    </LinearLayout>
</com.google.android.gms.ads.formats.NativeAppInstallAdView>


Переопределим в AdmobAdapterWrapper.java метод getView, чтобы адаптер-обертка имел возможность по типу отображаемого элемента создать нужный View и опубликовать в него данные

Развернуть
@Override
public View getView(int position, View convertView, ViewGroup parent) {
        switch (getItemViewType(position)) {
            case VIEW_TYPE_AD_INSTALL:
                NativeAppInstallAdView lvi1;
                NativeAppInstallAd ad1 = (NativeAppInstallAd) getItem(position);
                if (convertView == null) {
                    lvi1 = getInstallAdView(parent, ad1);
                } else {
                    lvi1 = (NativeAppInstallAdView) convertView;
                    bindInstallAdView(lvi1, ad1);
                }
                return lvi1;
            case VIEW_TYPE_AD_CONTENT:
                NativeContentAdView lvi2;
                NativeContentAd ad2 = (NativeContentAd) getItem(position);
                if (convertView == null) {
                    lvi2 = getContentAdView(parent, ad2);
                } else {
                    lvi2 = (NativeContentAdView) convertView;
                    bindContentAdView(lvi2, ad2);
                }
                return lvi2;
            default:
                int origPos = getOriginalContentPosition(position);
                return mAdapter.getView(origPos, convertView, parent);
        }
}


Где в случае если отображаемый элемент списка – это реклама, показываем View для соответствующего рекламного блока, в противном случае в секции default показываем View из mAdapter.

Для создания новых экземпляров View рекламы добавим следующие методы для обоих типов рекламы:

Развернуть
private NativeContentAdView getContentAdView(ViewGroup parent, NativeContentAd ad) {
        LayoutInflater inflater = (LayoutInflater) parent.getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        NativeContentAdView adView = (NativeContentAdView) inflater
                .inflate(getContentAdsLayoutId(), parent, false);
        bindContentAdView(adView, ad);
        return adView;
}

private NativeAppInstallAdView getInstallAdView(ViewGroup parent, NativeAppInstallAd ad) {
        LayoutInflater inflater = (LayoutInflater) parent.getContext()
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        NativeAppInstallAdView adView = (NativeAppInstallAdView) inflater
                .inflate(getInstallAdsLayoutId(), parent, false);
        bindInstallAdView(adView, ad);
        return adView;
}


5. Публикация данных из рекламного блока в layout


Для того, чтобы опубликовать данные из рекламного блока реализуем в AdmobAdapterWrapper.java следующие методы:

Развернуть
private void bindContentAdView(NativeContentAdView adView, NativeContentAd ad) {
        if (adView == null || ad == null) return;
        TextView tvHeader = (TextView) adView.findViewById(R.id.tvHeader);
        tvHeader.setText(ad.getHeadline());
        adView.setHeadlineView(tvHeader);
        TextView tvDescription = (TextView) adView.findViewById(R.id.tvDescription);
        tvDescription.setText(ad.getBody());
        adView.setBodyView(tvDescription);
        ImageView ivLogo = (ImageView) adView.findViewById(R.id.ivLogo);
        if(ad.getLogo()!=null)
            ivLogo.setImageDrawable(ad.getLogo().getDrawable());
        adView.setLogoView(ivLogo);
        Button btnAction = (Button) adView.findViewById(R.id.btnAction);
        btnAction.setText(ad.getCallToAction());
        adView.setCallToActionView(btnAction);
        TextView tvAdvertiser = (TextView) adView.findViewById(R.id.tvAdvertiser);
        tvAdvertiser.setText(ad.getAdvertiser());
        adView.setAdvertiserView(tvAdvertiser);
        ImageView ivImage = (ImageView) adView.findViewById(R.id.ivImage);
        if (ad.getImages() != null && ad.getImages().size() > 0) {
            ivImage.setImageDrawable(ad.getImages().get(0).getDrawable());
            ivImage.setVisibility(View.VISIBLE);
        } else ivImage.setVisibility(View.GONE);
        adView.setImageView(ivImage);
        adView.setNativeAd(ad);
}

private void bindInstallAdView(NativeAppInstallAdView adView, NativeAppInstallAd ad) {
        if (adView == null || ad == null) return;
        TextView tvHeader = (TextView) adView.findViewById(R.id.tvHeader);
        tvHeader.setText(ad.getHeadline());
        adView.setHeadlineView(tvHeader);
        TextView tvDescription = (TextView) adView.findViewById(R.id.tvDescription);
        tvDescription.setText(ad.getBody());
        adView.setBodyView(tvDescription);
        ImageView ivLogo = (ImageView) adView.findViewById(R.id.ivLogo);
        if(ad.getIcon()!=null)
            ivLogo.setImageDrawable(ad.getIcon().getDrawable());
        adView.setIconView(ivLogo);
        Button btnAction = (Button) adView.findViewById(R.id.btnAction);
        btnAction.setText(ad.getCallToAction());
        adView.setCallToActionView(btnAction);
        TextView tvStore = (TextView) adView.findViewById(R.id.tvStore);
        tvStore.setText(ad.getStore());
        adView.setStoreView(tvStore);
        TextView tvPrice = (TextView) adView.findViewById(R.id.tvPrice);
        tvPrice.setText(ad.getPrice());
        adView.setPriceView(tvPrice);
        ImageView ivImage = (ImageView) adView.findViewById(R.id.ivImage);
        if (ad.getImages() != null && ad.getImages().size() > 0) {
            ivImage.setImageDrawable(ad.getImages().get(0).getDrawable());
            ivImage.setVisibility(View.VISIBLE);
        } else ivImage.setVisibility(View.GONE);
        adView.setImageView(ivImage);
        adView.setNativeAd(ad);
    }
}


Методы схожи между собой. Основной момент – это регистрация всех элементов из созданных нами layout в экземпляре NativeAdView, например:

TextView tvHeader = (TextView) adView.findViewById(R.id.tvHeader);
tvHeader.setText(ad.getHeadline());
//регистрируем в adView заголовок рекламы tvHeader
adView.setHeadlineView(tvHeader);

В конце обоих методов в NativeAdView регистрируется и сам объект NativeAd. Итак, имея в разметке (например, в res\layout\activity_main.xml) список:

<ListView android:id="@+id/lvMessages"
 android:layout_width="match_parent"
 android:layout_height="match_parent"/>

Можно использовать AdmobAdapterWrapper следующим образом (в некоем классе Activity, который отображает данный список).

Развернуть
ListView lvMessages;
AdmobAdapterWrapper adapterWrapper;
//...
@Override
public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initListViewItems();
}

private void initListViewItems() {
        lvMessages = (ListView) findViewById(R.id.lvMessages);
        //создаем адаптер с данными
        ArrayAdapter<String> adapter  = new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_1);
        adapterWrapper = new AdmobAdapterWrapper(this);
        adapterWrapper.setAdapter(adapter); //агрегируем адаптер с данными в адаптер-обертку.
        //можно указать собственные layout для блоков рекламы
        //adapterWrapper.setInstallAdsLayoutId(R.layout.your_installad_layout);
        //adapterWrapper.setcontentAdsLayoutId(R.layout.your_installad_layout);
        //устанавливаем предельное количество блоков рекламы в списке
        adapterWrapper.setLimitOfAds(3);
        //устанавливаем интервал в 10 элементов с данными между блоками с рекламой.
        //Следует следить за тем, чтобы соблюдались правила и рекомендации Admob's по отображению   
        //рекламы, для чего учитывать высоту элементов в списке и разрешение различных гаджетов.
        adapterWrapper.setNoOfDataBetweenAds(10);
        lvMessages.setAdapter(adapterWrapper); // указываем списку на адаптер-обертку
        //заполняем список тестовыми данными
        final String sItem = "item #";
        ArrayList<String> lst = new ArrayList<String>(100);
        for(int i=1;i<=100;i++)
            lst.add(sItem.concat(Integer.toString(i)));
        adapter.addAll(lst);
        adapter.notifyDataSetChanged();
}

//Чистим использованные ресурсы
@Override
protected void onDestroy() {
        super.onDestroy();
        adapterWrapper.destroyAds();
}


Результат может быть примерно таким, как показано на «гифке».

image

Выводы


Весь код можно посмотреть тут. Так как времени на развитие библиотеки не очень много, буду рад вашим «форкам». В ближайших планах на развитие следующие задачи:

  1. Обеспечить возможность указывать AdmobAdapterWrapper свои методы bindInstallAdView и bindContentAdView, чтобы использовать свои layout для рекламных блоков без изменения кода библиотеки.
  2. Оформить библиотеку как Gradle Android library в репозиторий jCenter.
  3. Добавить поддержку для RecyclerView

В заключение, подчеркну основные моменты, о которых не стоит забывать при добавлении нативной рекламы в ваше приложение:
• Не забывайте про правила публикации рекламы в мобильное приложение: не более 3 баннеров рекламы на 1 странице с прокручиваемым контентом, при этом в видимой части экрана одновременно отображается только 1 баннер
• Во время отладки рекомендуется использовать именно тестовый UnitId, в противном случае Admob может посчитать клики по рекламным блокам за накрутку и заблокировать ваш аккаунт.
• Если при загрузке рекламы в logcat выдает сообщения типа
W/Ads﹕ Failed to load ad: 0
, как описано, например, тут попробуйте запускать не на эмуляторе, а на реальном устройстве, причем без доступа к руту. Еще одной причиной может быть недостаток рекламы на сервере Admob, в этом случае следует подождать некоторое время после первого запуска и попробовать еще раз (именно так было у меня).

Желаю вам красивой нативной рекламы в ваших приложениях!
Clockbyte @kot331107
карма
6,0
рейтинг –0,8
разработчик приложений под Android
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (12)

  • 0
    Спасибо за статью. А есть уже аналитика по сравнению доходности банерной и нативной рекламы?
    • 0
      Пожалуйста! Нет, к сожалению нативная реклама пока находится в стадии бета-релиза (вроде бы я указывал это во вводной части к статье), и доходность по ней не рассчитывается. Если я ошибаюсь, прошу меня поправить…
  • 0
    У меня вручную интегрирована реклама в списки. Grid view. Если честно, я не в восторге.
    • 0
      А поделитесь пож-та скриншотом/примером, как размещали в GridView. Интересно же :) От чего вы не в восторге? Не нравится как реклама выглядит или время загрузки не устраивает?
      • 0
        Ну не нравится скорее всего из-за моих особенностей реализации. Получается, что при загрузке списка, загружается AdView. (чтобы потом сразу его показать) Но в итоге это выливается в очень маленький RPM. Скорее всего заменю на рекламу поверх списка + кнопку скрытия. Просто решил рекламу между элементов, чтобы соблюдать «реклама не должна быть близко к кнопкам управления».
        Смотрится конечно неплохо, но профит ужасный.
        habrastorage.org/files/94c/fa8/94c/94cfa894cecd4170bec18c085e6183f3.png
        habrastorage.org/files/8e9/a3f/e78/8e9a3fe7806746578d277eafbc0aacb7.png
        • 0
          Я вас понял, это все же баннерная реклама, я вначале подумал что вы интегрировали в GridView нативную рекламу… может быть попробуете ее (когда ее окончательно выпустят в релиз, разумеется)? Мне кажется она «ляжет» между элементами как влитая если написать годный layout. Еще я бы немного увеличил размер элементов, чтобы вместить в «квадрат» минимально требуемый объем полей рекламы. Если возьметесь, опишите свой опыт пож-та :)
        • 0
          Прошу прощения, что отвечаю в несколько постов. Хочу еще отметить, что в том способе реализации который я описывал в статье, пользователь увидит сначала контент списка без «пробелов» и с возможностью прокрутки, а реклама грузится параллельно, и отображается, что называется, «по готовности», просто вклиниваясь между элементами. Положение скролла при этом не меняется и не дергается, все адекватно. Это на мой взгляд серьезно улучшает качество UI, особенно учитывая пользовательскую нелюбовь к рекламе :)
          • 0
            То есть, пользователь собирается кликнуть, а тут «по готовности» отображается реклама вместо элемента?
            • 0
              Вы знаете, тут надо смотреть по месту, конечно. В моем приложении с ListView такой момент маловероятен, так как пока пользователь «докрутит» до того места где я отображаю рекламный блок, он уже успевает загрузиться (даже при медленном интернете) и занять свое место. В данный момент, например, я показываю только один блок на 6-й позиции.
            • 0
              Впрочем, это особенности реализации UI и не о них речь. Можно добавить нативную рекламу и выделять под нее место в списке/GridView сразу, еще до ее загрузки. Конечно это повлияет на эстетику, но хоть неосознанно кликать не будут :)
              • 0
                У меня так и сделано
                • 0
                  да, вы об этом уже писали и я вас услышал. но если вернуться к началу нашей беседы, то я предложил как вариант попробовать нативную рекламу (после ее релиза). Она может смотреться эффектнее, так как layout под нее вы создадите сами и размеры будете определять также вы.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.